auto backup: 2026-05-10 08:10:01
This commit is contained in:
parent
54c7d9c335
commit
f7661fc026
11
MEMORY.md
11
MEMORY.md
@ -91,6 +91,17 @@
|
||||
|
||||
## 经验教训
|
||||
|
||||
### bitable写入:嵌套JSON的Shell转义陷阱(2026-05-08)
|
||||
- `operate_bitable.sh` 通过 shell 变量展开传递嵌套 JSON 时,内部双引号会被 shell 提前解析,导致数据截断(如 jsonData 的 questionSet 全部清空)
|
||||
- **解决方案:** 对于包含嵌套 JSON 的字段(如 jsonData),直接用 Python subprocess 调用 curl,构造完整 request body,避免经过 shell 变量展开
|
||||
- 写入后必须回读验证(不仅是 record_id 校验,还要检查 jsonData 内容的完整性)
|
||||
|
||||
### 单元挑战互动组件多维表格 App Token(2026-05-08)
|
||||
- **App Token:** `CMHSbUUjka3TrUsaxxEc297ongf`
|
||||
- **用途:** 存储所有单元挑战互动组件配置数据
|
||||
- **包含表格:** 听力-P1~P7、阅读-P1~P7、写作-P1~P5、口语-P1~P5 等
|
||||
- **操作方式:** Bot身份通过 curl 直接调用 bitable API
|
||||
|
||||
(在此记录工作中总结的经验教训,供后续参考)
|
||||
|
||||
---
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
{"type":"memory.recall.recorded","timestamp":"2026-05-07T08:48:55.437Z","query":"王璐辰 user_id permissions","resultCount":1,"results":[{"path":"memory/2026-05-07.md","startLine":57,"endLine":74,"score":0.6621249966506227}]}
|
||||
{"type":"memory.recall.recorded","timestamp":"2026-05-08T01:18:18.412Z","query":"writing_pic_qa 写作看图回答题 生产规则","resultCount":3,"results":[{"path":"memory/2026-05-07.md","startLine":215,"endLine":235,"score":1},{"path":"memory/2026-05-07.md","startLine":199,"endLine":219,"score":1},{"path":"memory/2026-05-07.md","startLine":1,"endLine":20,"score":1}]}
|
||||
{"type":"memory.recall.recorded","timestamp":"2026-05-09T02:03:53.176Z","query":"reading_pic_judge bitable 多维表格 table_id 121301","resultCount":1,"results":[{"path":"memory/2026-05-07.md","startLine":1,"endLine":20,"score":1}]}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"updatedAt": "2026-05-08T01:18:18.412Z",
|
||||
"updatedAt": "2026-05-09T02:03:53.176Z",
|
||||
"entries": {
|
||||
"memory:memory/2026-05-07.md:57:74": {
|
||||
"key": "memory:memory/2026-05-07.md:57:74",
|
||||
@ -102,18 +102,20 @@
|
||||
"endLine": 20,
|
||||
"source": "memory",
|
||||
"snippet": "# 2026-05-07 工作日志 ## 会话记录 ### 刘彦江 (ou_5af74c1fb96042e33cc0f16b5ca02cf4) — 单元挑战新增3个题型 - **时间:** 11:19 ~ 11:28 - **需求:** 单元挑战新增3个题型:阅读看图回答题(reading_pic_qa)、阅读看图判断题(reading_pic_judge)、写作看图回答题(writing_pic_qa) - **状态:** ✅ 已完成题型规范落地 - **交付内容:** 1. 创建3个题型SKILL.md: - `reading/common/reading_pic_qa/SKILL.md` — 阅读看图回答题(共享大图+多题Yes/No判断) - `reading/common/reading_pic_judge/SKILL.md` — 阅读看图判断题(每题独立配图+Yes/No判断) - `writing/common/writing_pic_qa/SKILL.md` — 写作看图回答题(看图+提示答案开头+填空) 2. 更新 `MEMORY.md` 全题型阶段归属规则 3. 明确题型JSON结构、难度等级(A/B/C/D四级)、能力标签、校验规则 - **关键决策:** 3个题型均为 L1&L2 共用,通过难度参数区分阶段;reading_pic_qa 共享大图,reading_pic_judge 每题独立配图 ### 李应瑛 (ou_1bd7317ae2ccfeb57e1132028847279e) — 单词表对比请求",
|
||||
"recallCount": 1,
|
||||
"recallCount": 2,
|
||||
"dailyCount": 0,
|
||||
"groundedCount": 0,
|
||||
"totalScore": 1,
|
||||
"totalScore": 2,
|
||||
"maxScore": 1,
|
||||
"firstRecalledAt": "2026-05-08T01:18:18.412Z",
|
||||
"lastRecalledAt": "2026-05-08T01:18:18.412Z",
|
||||
"lastRecalledAt": "2026-05-09T02:03:53.176Z",
|
||||
"queryHashes": [
|
||||
"390d35f8d143"
|
||||
"390d35f8d143",
|
||||
"fd4c9b7de37b"
|
||||
],
|
||||
"recallDays": [
|
||||
"2026-05-08"
|
||||
"2026-05-08",
|
||||
"2026-05-09"
|
||||
],
|
||||
"conceptTags": [
|
||||
"reading-pic-qa",
|
||||
|
||||
@ -27,6 +27,24 @@
|
||||
| 111101 | earth,horse,donkey,duck,air | in front of,behind,arm,between,next to | 10q: 6Y4N | recvj0SWMKpQxG |
|
||||
| 111201 | lorry,garden,flower,window,these | listen,clap,night,morning | 9q: 5Y4N | recvj0SXQtpXkd |
|
||||
|
||||
- **111201 题组2 仅4题**(用户提供4个知识点:listen/clap/night/morning)
|
||||
- **111201 题组2 仅4题**(用户提供4个知识点:listen/clap/night/morning,后补close→5题)
|
||||
- **设计规则:** L1 A级 1-4词极简陈述句;每题独立单物品/简单场景图;Yes题图与题干匹配、No题图与题干矛盾
|
||||
- **bitable写入:** 首轮create_record未正确写入(shell参数截断),后用Python update_record逐一修复
|
||||
|
||||
### 刘彦江 (ou_5af74c1fb96042e33cc0f16b5ca02cf4) — reading_pic_judge 阶段2生产(4套 ID 121301-121601)
|
||||
- **时间:** ~17:40 ~ 18:00
|
||||
- **状态:** ✅ 已完成(4套/40题,全量写入+回读验证通过)
|
||||
- **目标bitable:** 阅读-P6-看图判断题
|
||||
- App: `CMHSbUUjka3TrUsaxxEc297ongf`
|
||||
- Table: `tblJc60aO0T163MJ`
|
||||
- **能力标签:** `["图文判断", "句图一致性"]`
|
||||
- **词汇校验:** 40词中仅8词在L1词库(sofa/zoo/sock/student/school/teacher/mat/mouth),32词不在词库。已提示用户但不禁产。
|
||||
|
||||
| ID | 题组1 | 题组2 | Yes/No | record_id |
|
||||
|----|------|------|--------|-----------|
|
||||
| 121301 | home,house,sofa,cupboard,bedroom | zoo,hippo,bear,jeans,sock | 6Y4N | recvj5nIAxF3XG |
|
||||
| 121401 | student,school,teacher,crayon,board | mat,letter,desk,schoolbag,handbag | 6Y4N | recvj5nJG33bBE |
|
||||
| 121501 | body,head,mouth,ear,friend | camera,boy,girl,skateboarding,skateboard | 6Y4N | recvj5nKxhm8X6 |
|
||||
| 121601 | summer,autumn,winter,cold,spring | bus,taxi,trousers,shorts,snow | 6Y4N | recvj5nLUpubaU |
|
||||
|
||||
- **经验教训:** `operate_bitable.sh` 在 shell 中展开嵌套 JSON 会导致数据截断(jsonData questionSet 全部丢失,textTitle 清空)。后续写入多维表格特别是嵌套 JSON 字段时,应直接用 Python curl_post 绕过 shell 转义问题。
|
||||
|
||||
5
memory/2026-05-09-0941.md
Normal file
5
memory/2026-05-09-0941.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Session: 2026-05-09 09:41:44 GMT+8
|
||||
|
||||
- **Session Key**: agent:xiaoyan:feishu:direct:ou_5af74c1fb96042e33cc0f16b5ca02cf4
|
||||
- **Session ID**: bb724282-4006-4ad9-9d71-a5abdd685632
|
||||
- **Source**: feishu
|
||||
47
memory/2026-05-09-1030.md
Normal file
47
memory/2026-05-09-1030.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Session: 2026-05-09 10:30:50 GMT+8
|
||||
|
||||
- **Session Key**: agent:xiaoyan:feishu:direct:ou_270924c22bf8d63cf356dbd37614055f
|
||||
- **Session ID**: 7dc15047-1c7b-4739-bafc-245832c64c9b
|
||||
- **Source**: feishu
|
||||
|
||||
## Conversation Summary
|
||||
|
||||
user: 你的 workspace 自动备份任务, 还在吗? 我发现 云仓库里 最近推送一次是两周前 正常应该每天早上推送
|
||||
assistant: cron 任务还在,每天 08:10 执行。让我检查日志和脚本执行情况:
|
||||
assistant: 日志目录不存在。让我手动试跑一下备份脚本看报什么错:
|
||||
assistant: 脚本能正常跑,log 目录也创建了。现在来看看为什么 cron 没触发——先确认 cron 服务状态和系统日志:
|
||||
assistant: Cron 每天 08:10 都在执行!但日志目录在脚本运行前不存在(shell 重定向时目录还没建),导致输出全丢了。让我确认根因:
|
||||
assistant: 根因找到了。看下完整诊断:
|
||||
|
||||
**cron 任务本身每天都在 08:10 准时执行**(syslog 确认 5月5/6/7/8日都有触发)。问题出在 `logs/` 目录丢失导致 shell 重定向失败,脚本输出全部丢失。
|
||||
|
||||
具体链路:
|
||||
1. cron 命令里 `>> .../logs/git_backup.log` 的重定向在脚本启动**之前**由 shell 执行
|
||||
2. 脚本里的 `mkdir -p logs/` 跑在重定向之后,来不及救
|
||||
3. `logs/` 目录消失后,所有 cron 运行的输出(包括错误信息)都被丢弃
|
||||
4. 无法确认脚本是 push 失败还是根本没 commit
|
||||
|
||||
**修复方案:在 cron 命令里前置创建目录。**
|
||||
assistant: 现在修复。做两件事:1) 修 cron 命令确保日志目录存在;2) 确认推送已经恢复正常。
|
||||
assistant: sed 处理特殊字符有问题。让我用 Python 精确处理:
|
||||
assistant: cron 已修复。刚才手动跑的时候已经推送成功了,验证一下远程:
|
||||
assistant: 已确认远程仓库里最新一次推送就是刚才的 `82dc242 auto backup: 2026-05-08 19:32:05`。
|
||||
|
||||
## 诊断总结
|
||||
|
||||
| 项目 | 状态 |
|
||||
|------|------|
|
||||
| cron 任务 | ✅ 一直在,每天 08:10 准时触发(syslog 可查) |
|
||||
| 远程最后推送 | ❌ 停在 4月23日(两周前) |
|
||||
| 根因 | `logs/` 目录丢失 → shell 重定向 `>> logs/git_backup.log` 在脚本启动前失败 → 所有输出被丢弃,无法看到错误 |
|
||||
| 修复 | cron 命令前置 `mkdir -p .../logs`,确保日志目录在重定向前已存在 |
|
||||
| 当前状态 | ✅ 刚手动推送成功,远程已更新到最新 |
|
||||
|
||||
**具体原因:** backup_workspace.sh 脚本内部有 `mkdir -p logs/`,但 cron 命令里的 `>> logs/git_backup.log` 重定向是 shell 在**启动脚本之前**执行的。`logs/` 目录不知何时被清理掉后,重定向静默失败,cron 无论成功失败都看不到输出。
|
||||
|
||||
**修复内容:** crontab 中小研的备份行改为:
|
||||
```
|
||||
10 8 * * * mkdir -p /root/.openclaw/workspace-xiaoyan/logs && /bin/bash .../backup_workspace.sh >> .../logs/git_backup.log 2>&1
|
||||
```
|
||||
|
||||
明天早上 08:10 会自动跑,要不要我现在再手动跑一次确认完全没问题?
|
||||
27
memory/2026-05-09-1055.md
Normal file
27
memory/2026-05-09-1055.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Session: 2026-05-09 10:55:15 GMT+8
|
||||
|
||||
- **Session Key**: agent:xiaoyan:feishu:direct:ou_270924c22bf8d63cf356dbd37614055f
|
||||
- **Session ID**: 32e1540d-6366-4606-b2aa-7ed88688b2b1
|
||||
- **Source**: feishu
|
||||
|
||||
## Conversation Summary
|
||||
|
||||
user: 组件配置json https://makee-interactive.feishu.cn/wiki/AEPJwf0DUiDkWska4SbceSAbnyc
|
||||
assistant: 处理完成。以下是本次组件配置JSON生成结果汇总:
|
||||
|
||||
| 项目 | 数量 |
|
||||
|------|------|
|
||||
| 识别组件总数 | **42** |
|
||||
| 成功生成 | **41** |
|
||||
| 失败 | 0 |
|
||||
| 跳过 | 1(口语独白,生成器尚未实现) |
|
||||
|
||||
**覆盖的组件类型:**
|
||||
|
||||
**中互动组件(25个):** 对话朗读、对话挖空、对话组句、对话表达、对话选择、对话选读、信息描写、信息拼词、信息组句、信息补词、信息填词、信息填句、图片单选、图片多选、图片有序、图片拖拽
|
||||
|
||||
**核心互动组件(12个全部覆盖):** 合作阅读、看图选词、口语快答、口语妙问、看图说话、合作听力、听力拖拽、听力选择、看图组句、看图撰写、邮件组句、邮件撰写
|
||||
|
||||
**文档:** L1-S2-U14-L1 到你上学啦 (组件录入测试-全量)
|
||||
|
||||
📄 HTML 报告已通过飞书 Bot 发送给你,可直接在浏览器中打开查看详情并推送配置到远程数据库。
|
||||
39
memory/2026-05-09-1107.md
Normal file
39
memory/2026-05-09-1107.md
Normal file
@ -0,0 +1,39 @@
|
||||
# Session: 2026-05-09 11:07:40 GMT+8
|
||||
|
||||
- **Session Key**: agent:xiaoyan:feishu:direct:ou_270924c22bf8d63cf356dbd37614055f
|
||||
- **Session ID**: 88d0af85-19a7-4241-9858-b7e3e8da2947
|
||||
- **Source**: feishu
|
||||
|
||||
## Conversation Summary
|
||||
|
||||
user: 组件配置json https://makee-interactive.feishu.cn/wiki/AEPJwf0DUiDkWska4SbceSAbnyc
|
||||
assistant: 李若松,收到。启动组件配置JSON生成流水线,处理文档:
|
||||
`https://makee-interactive.feishu.cn/wiki/AEPJwf0DUiDkWska4SbceSAbnyc`
|
||||
assistant: 流水线正在执行中,等待处理完成...
|
||||
assistant: 流水线正在并行处理 42 个组件,进度良好。继续等待...
|
||||
assistant: 进度 21/42,继续...
|
||||
assistant: 进度 24/42,核心互动组件陆续完成中...
|
||||
assistant: 写作组件处理中,进度 25/42...
|
||||
assistant: 接近完成,39/42...
|
||||
assistant: 流水线执行完毕。报告已发送到你的飞书私聊。
|
||||
|
||||
---
|
||||
|
||||
**📊 处理报告:`L1-S2-U14-L1 到你上学啦 (组件录入测试-全量)`**
|
||||
|
||||
| 统计 | 数量 |
|
||||
|------|------|
|
||||
| 总组件 | **42** |
|
||||
| 成功 | **41** |
|
||||
| 跳过 | **1**(口语独白,尚未实现生成器) |
|
||||
| 失败 | **0** |
|
||||
|
||||
**覆盖类型:**
|
||||
|
||||
中互动(16种):对话朗读×8、对话挖空×4、对话组句×4、对话表达×1、对话选择×1、对话选读×1、信息描写×1、信息拼词×1、信息组句×1、信息补词×1、信息填词×1、信息填句×1、图片单选×1、图片多选×1、图片有序×1、图片拖拽×1
|
||||
|
||||
核心互动(11种):合作阅读、看图选词、口语快答、口语妙问、看图说话、合作听力、听力拖拽、听力选择×2、看图组句、看图撰写、邮件组句、邮件撰写
|
||||
|
||||
---
|
||||
|
||||
📄 HTML 报告已通过飞书 Bot 发给你,可在浏览器中打开查看完整配置 JSON 并进行推送操作。
|
||||
@ -74,6 +74,36 @@ python3 scripts/pipeline.py "<wiki_url>" --send-to <user_id>
|
||||
- pipeline 执行时自动检测并启动代理服务
|
||||
- 目标接口:`POST https://api-test.valavala.com/content/feishu/notify`
|
||||
|
||||
### 代理服务自动管理
|
||||
|
||||
pipeline 在生成 HTML 报告前会自动管理代理服务,运行时会输出 `[代理]` 前缀的状态信息:
|
||||
|
||||
| 输出信息 | 含义 |
|
||||
|----------|------|
|
||||
| `[代理] ✓ 中转服务正常 → <url>` | 代理服务运行正常,HTML 将使用中转地址 |
|
||||
| `[代理] ⚠️ 端口被僵死进程占用,正在清理...` | 检测到旧进程卡死,自动 kill 并重启 |
|
||||
| `[代理] ✓ 中转服务启动成功 → <url>` | 代理重启成功 |
|
||||
| `[代理] ❌ 中转服务启动失败!` | 代理无法启动,HTML 回退为直连 HTTPS 地址 |
|
||||
|
||||
**自动恢复机制**:当健康检查失败但端口被占用时(僵死进程),pipeline 会自动终止僵死进程并重新启动代理,无需人工干预。
|
||||
|
||||
**手动管理代理**:
|
||||
```bash
|
||||
# 查看代理进程
|
||||
ss -tlnp | grep 18080
|
||||
|
||||
# 手动启动
|
||||
python3 scripts/proxy_server.py &
|
||||
|
||||
# 手动停止
|
||||
kill $(ss -tlnp | grep 18080 | grep -oP 'pid=\K\d+')
|
||||
|
||||
# 健康检查
|
||||
curl http://127.0.0.1:18080/health
|
||||
```
|
||||
|
||||
**注意**:如果 HTML 报告中显示的推送地址是 `https://api-test.valavala.com/...` 而非 `http://<external_ip>:18080/api/push`,说明生成时代理服务未能正常运行。可在 HTML 顶部地址栏手动修改为中转地址,或重新运行 pipeline 生成。
|
||||
|
||||
## 概述
|
||||
|
||||
本skill实现从**飞书wiki剧本文档** → **结构化组件配置JSON**的完整生产流水线。
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -51,6 +51,7 @@ def _build_component_data(result):
|
||||
"category": category,
|
||||
"type_name": result.get("type_name", ""),
|
||||
"status": status,
|
||||
"raw_config": result.get("raw_config", ""),
|
||||
}
|
||||
|
||||
if status not in ("generated", "saved"):
|
||||
@ -252,6 +253,11 @@ def _render_component(idx, result):
|
||||
知识推送<span class="push-icon" id="kp-status-{idx}"></span>
|
||||
</button>'''
|
||||
|
||||
# Regenerate button (for all components)
|
||||
regen_btn = f'''<button class="push-btn regen-btn" data-idx="{idx}" onclick="event.stopPropagation();toggleRegenPanel({idx})">
|
||||
重新生成<span class="push-icon" id="regen-status-{idx}"></span>
|
||||
</button>'''
|
||||
|
||||
# Build body
|
||||
body_html = ""
|
||||
if status in ("generated", "saved"):
|
||||
@ -292,17 +298,22 @@ def _render_component(idx, result):
|
||||
elif status == "failed":
|
||||
err = html.escape(result.get("error", "未知错误"))
|
||||
body_html = f'<div class="error-msg">错误: {err}</div>'
|
||||
raw_config = result.get("raw_config", "")
|
||||
if raw_config:
|
||||
escaped_config = html.escape(raw_config)
|
||||
body_html += f'<div class="raw-config-section"><div class="raw-config-label">原始组件配置</div><pre class="raw-config-pre">{escaped_config}</pre></div>'
|
||||
elif status == "skipped":
|
||||
reason = html.escape(result.get("reason", ""))
|
||||
body_html = f'<div class="skip-msg">跳过: {reason}</div>'
|
||||
raw_config = result.get("raw_config", "")
|
||||
if raw_config:
|
||||
escaped_config = html.escape(raw_config)
|
||||
body_html += f'<div class="raw-config-section"><div class="raw-config-label">原始组件配置</div><pre class="raw-config-pre">{escaped_config}</pre></div>'
|
||||
|
||||
# Regenerate panel (editable teaching_config) — for all components
|
||||
raw_config = result.get("raw_config", "")
|
||||
escaped_config = html.escape(raw_config)
|
||||
config_rows = min(max(raw_config.count('\n') + 1, 5), 25)
|
||||
# For failed/skipped: show config panel expanded; for success: collapsed
|
||||
regen_display = "block" if status in ("failed", "skipped") else "none"
|
||||
regen_panel = f'''
|
||||
<div class="regen-panel" id="regen-panel-{idx}" style="display:{regen_display};">
|
||||
<div class="regen-label">原始组件配置 <span class="regen-hint">(可编辑后点击"执行重新生成")</span></div>
|
||||
<textarea class="regen-textarea" id="regen-config-{idx}" rows="{config_rows}">{escaped_config}</textarea>
|
||||
<button class="regen-exec-btn" id="regen-exec-{idx}" onclick="regenerateComponent({idx})">执行重新生成</button>
|
||||
</div>'''
|
||||
|
||||
return f'''
|
||||
<div class="comp-card" id="card-{idx}">
|
||||
@ -312,10 +323,15 @@ def _render_component(idx, result):
|
||||
<span class="comp-cid">cId: {html.escape(str(cId))}</span>
|
||||
<span class="comp-ctype">{html.escape(str(cType))}</span>
|
||||
{push_btns}
|
||||
<span class="comp-status {status_class}">{status_label}</span>
|
||||
{regen_btn}
|
||||
<span class="comp-status {status_class}" id="status-label-{idx}">{status_label}</span>
|
||||
<span class="comp-arrow" id="arrow-{idx}">▶</span>
|
||||
</div>
|
||||
<div class="comp-body" id="body-{idx}">{body_html}</div>
|
||||
<div class="comp-body" id="body-{idx}">
|
||||
{body_html}
|
||||
<div id="regen-result-{idx}"></div>
|
||||
{regen_panel}
|
||||
</div>
|
||||
</div>'''
|
||||
|
||||
|
||||
@ -335,6 +351,8 @@ def generate_html_report(report, output_path=None, model_name="", wiki_token="",
|
||||
# Serialize data for JS
|
||||
components_json = json.dumps(components_data, ensure_ascii=False)
|
||||
push_field_map_json = json.dumps(PUSH_FIELD_MAP, ensure_ascii=False)
|
||||
character_map_json = json.dumps(report.get("character_map") or {}, ensure_ascii=False)
|
||||
level_json = json.dumps(report.get("level") or None, ensure_ascii=False)
|
||||
|
||||
total = report.get("total_components", 0)
|
||||
success = report.get("success", 0)
|
||||
@ -440,6 +458,8 @@ body {{ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto San
|
||||
.config-btn:hover {{ background:#dbeafe; }}
|
||||
.kp-btn {{ background:#f0fdf4; color:#16a34a; border-color:#bbf7d0; }}
|
||||
.kp-btn:hover {{ background:#dcfce7; }}
|
||||
.regen-btn {{ background:#fef3c7; color:#d97706; border-color:#fde68a; }}
|
||||
.regen-btn:hover {{ background:#fde68a; }}
|
||||
.push-btn:disabled {{ opacity:0.5; cursor:not-allowed; }}
|
||||
.push-icon {{ display:inline-block; width:8px; height:8px; border-radius:50%; background:#94a3b8; }}
|
||||
.push-icon.ok {{ background:#10b981; }}
|
||||
@ -447,6 +467,20 @@ body {{ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto San
|
||||
.push-icon.loading {{ background:#f59e0b; animation:pulse 0.8s infinite; }}
|
||||
@keyframes pulse {{ 0%,100%{{opacity:1}} 50%{{opacity:0.3}} }}
|
||||
|
||||
/* Regenerate panel */
|
||||
.regen-panel {{ margin-top:12px; padding:12px; border:1px dashed var(--border); border-radius:8px; background:#fffbeb; }}
|
||||
.regen-label {{ font-size:13px; font-weight:600; color:#92400e; margin-bottom:6px; }}
|
||||
.regen-hint {{ font-weight:normal; font-size:11px; color:var(--text2); }}
|
||||
.regen-textarea {{ width:100%; font-family:"SF Mono",Monaco,Consolas,monospace; font-size:12px;
|
||||
border:1px solid #fde68a; border-radius:6px; padding:10px; background:#fff;
|
||||
resize:vertical; line-height:1.5; tab-size:2; }}
|
||||
.regen-textarea:focus {{ border-color:#d97706; outline:none; box-shadow:0 0 0 2px rgba(217,119,6,0.15); }}
|
||||
.regen-exec-btn {{ margin-top:8px; padding:8px 20px; border:none; border-radius:6px;
|
||||
background:#d97706; color:#fff; font-size:13px; font-weight:600;
|
||||
cursor:pointer; transition:background 0.2s; }}
|
||||
.regen-exec-btn:hover {{ background:#b45309; }}
|
||||
.regen-exec-btn:disabled {{ opacity:0.5; cursor:not-allowed; }}
|
||||
|
||||
/* JSON sections */
|
||||
.json-section {{ margin-bottom:12px; }}
|
||||
.json-section-label {{ font-weight:600; font-size:13px; color:var(--accent); margin-bottom:4px;
|
||||
@ -602,12 +636,22 @@ body {{ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto San
|
||||
// ============ Embedded Data ============
|
||||
const COMPONENTS = {components_json};
|
||||
const PUSH_FIELD_MAP = {push_field_map_json};
|
||||
const CHARACTER_MAP = {character_map_json};
|
||||
const LEVEL = {level_json};
|
||||
|
||||
function getApiUrl() {{
|
||||
const input = document.getElementById('apiUrlInput');
|
||||
return input ? input.value.trim() : "{api_url or 'https://api-test.valavala.com/content/feishu/notify'}";
|
||||
}}
|
||||
|
||||
function getRegenUrl() {{
|
||||
const pushUrl = getApiUrl();
|
||||
// 从 push URL 推导 regenerate URL(同一服务器)
|
||||
const idx = pushUrl.indexOf('/api/');
|
||||
if (idx > 0) return pushUrl.substring(0, idx) + '/api/regenerate';
|
||||
return pushUrl.replace(/\\/[^\\/]*$/, '/api/regenerate');
|
||||
}}
|
||||
|
||||
// ============ Card Toggle ============
|
||||
function toggleCard(idx) {{
|
||||
const card = document.getElementById('card-' + idx);
|
||||
@ -1052,6 +1096,181 @@ async function pushAll() {{
|
||||
}}
|
||||
}}
|
||||
|
||||
// ============ Regenerate ============
|
||||
function toggleRegenPanel(idx) {{
|
||||
const panel = document.getElementById('regen-panel-' + idx);
|
||||
if (!panel) return;
|
||||
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
|
||||
}}
|
||||
|
||||
async function regenerateComponent(idx) {{
|
||||
const comp = COMPONENTS[idx - 1];
|
||||
const ta = document.getElementById('regen-config-' + idx);
|
||||
const btn = document.getElementById('regen-exec-' + idx);
|
||||
if (!ta || !btn) return;
|
||||
|
||||
const teachingConfig = ta.value.trim();
|
||||
if (!teachingConfig) {{
|
||||
showToast('原始组件配置不能为空', 'error');
|
||||
return;
|
||||
}}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = '生成中...';
|
||||
setStatus('regen-status-' + idx, 'loading');
|
||||
|
||||
const regenUrl = getRegenUrl();
|
||||
|
||||
try {{
|
||||
const resp = await fetch(regenUrl, {{
|
||||
method: 'POST',
|
||||
headers: {{ 'Content-Type': 'application/json' }},
|
||||
body: JSON.stringify({{
|
||||
teaching_config: teachingConfig,
|
||||
type_name: comp.type_name,
|
||||
cId: comp.cId,
|
||||
character_map: CHARACTER_MAP,
|
||||
level: LEVEL,
|
||||
}}),
|
||||
}});
|
||||
|
||||
const data = await resp.json();
|
||||
|
||||
if (!resp.ok) {{
|
||||
throw new Error(data.error || ('HTTP ' + resp.status));
|
||||
}}
|
||||
|
||||
// Update COMPONENTS data
|
||||
comp.status = 'generated';
|
||||
comp.category = data.category || comp.category;
|
||||
comp.cType = data.cType || comp.cType;
|
||||
comp.raw_config = teachingConfig;
|
||||
|
||||
if (comp.category === 'core') {{
|
||||
const fields = PUSH_FIELD_MAP[comp.cType] || [];
|
||||
for (const key of fields) {{
|
||||
if (data[key]) comp[key] = data[key];
|
||||
}}
|
||||
}} else {{
|
||||
if (data.jsonData) comp.jsonData = data.jsonData;
|
||||
}}
|
||||
if (data.kpInfo) comp.kpInfo = data.kpInfo;
|
||||
|
||||
// Rebuild body HTML
|
||||
rebuildComponentBody(idx, data);
|
||||
|
||||
setStatus('regen-status-' + idx, 'ok');
|
||||
btn.textContent = '执行重新生成';
|
||||
btn.disabled = false;
|
||||
|
||||
// Update status label
|
||||
const statusEl = document.getElementById('status-label-' + idx);
|
||||
if (statusEl) {{
|
||||
statusEl.className = 'comp-status ok';
|
||||
statusEl.textContent = '生成成功';
|
||||
}}
|
||||
|
||||
showToast('组件 #' + idx + ' 重新生成成功', 'success');
|
||||
}} catch(e) {{
|
||||
setStatus('regen-status-' + idx, 'fail');
|
||||
btn.textContent = '执行重新生成';
|
||||
btn.disabled = false;
|
||||
showToast('组件 #' + idx + ' 重新生成失败: ' + e.message, 'error');
|
||||
}}
|
||||
}}
|
||||
|
||||
function rebuildComponentBody(idx, data) {{
|
||||
const comp = COMPONENTS[idx - 1];
|
||||
const resultDiv = document.getElementById('regen-result-' + idx);
|
||||
if (!resultDiv) return;
|
||||
|
||||
// Clear old content above regen-result (error-msg, json-sections etc)
|
||||
const body = document.getElementById('body-' + idx);
|
||||
const regenPanel = document.getElementById('regen-panel-' + idx);
|
||||
// Remove everything except regen-result and regen-panel
|
||||
while (body.firstChild && body.firstChild !== resultDiv && body.firstChild !== regenPanel) {{
|
||||
body.removeChild(body.firstChild);
|
||||
}}
|
||||
|
||||
// Build new JSON sections
|
||||
let html = '';
|
||||
if (comp.category === 'core') {{
|
||||
const fields = PUSH_FIELD_MAP[comp.cType] || [];
|
||||
for (const key of fields) {{
|
||||
const val = data[key];
|
||||
if (val) {{
|
||||
html += buildJsonSectionHtml(key, key, val, idx);
|
||||
}}
|
||||
}}
|
||||
}} else {{
|
||||
if (data.jsonData) {{
|
||||
html += buildJsonSectionHtml('jsonData', 'jsonData', data.jsonData, idx);
|
||||
}}
|
||||
}}
|
||||
|
||||
if (data.kpInfo) {{
|
||||
const kpClean = JSON.parse(JSON.stringify(data.kpInfo));
|
||||
if (kpClean.kpInfo) {{
|
||||
kpClean.kpInfo.forEach(k => delete k.candidates);
|
||||
if (kpClean.kpInfo.length > 0) {{
|
||||
html += buildJsonSectionHtml('kpInfo JSON', 'kpInfo', kpClean, idx);
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
// Insert before resultDiv
|
||||
const temp = document.createElement('div');
|
||||
temp.innerHTML = html;
|
||||
while (temp.firstChild) {{
|
||||
body.insertBefore(temp.firstChild, resultDiv);
|
||||
}}
|
||||
|
||||
// Show push buttons in header if not already there
|
||||
ensurePushButtons(idx);
|
||||
}}
|
||||
|
||||
function buildJsonSectionHtml(label, key, data, idx) {{
|
||||
const raw = JSON.stringify(data, null, 2);
|
||||
const escaped = raw.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
const taId = 'ta-' + idx + '-' + key;
|
||||
const numLines = raw.split('\\n').length;
|
||||
const rows = Math.min(Math.max(numLines, 5), 30);
|
||||
let lineNums = '';
|
||||
for (let i = 1; i <= numLines; i++) lineNums += '<span>' + i + '</span>';
|
||||
|
||||
return '<div class="json-section">' +
|
||||
'<div class="json-section-label">' + escHtml(label) + '</div>' +
|
||||
'<div class="json-lined-wrap">' +
|
||||
'<div class="json-line-nums" id="ln-' + taId + '">' + lineNums + '</div>' +
|
||||
'<textarea id="' + taId + '" data-idx="' + idx + '" data-key="' + key + '" class="json-textarea" rows="' + rows + '" oninput="syncLineNums(this)" onscroll="this.previousElementSibling.scrollTop=this.scrollTop">' + escaped + '</textarea>' +
|
||||
'</div></div>';
|
||||
}}
|
||||
|
||||
function ensurePushButtons(idx) {{
|
||||
const header = document.querySelector('#card-' + idx + ' .comp-header');
|
||||
if (!header) return;
|
||||
// Check if push buttons already exist
|
||||
if (header.querySelector('.config-btn')) return;
|
||||
// Find regen-btn to insert before it
|
||||
const regenBtn = header.querySelector('.regen-btn');
|
||||
if (!regenBtn) return;
|
||||
|
||||
const cfgBtn = document.createElement('button');
|
||||
cfgBtn.className = 'push-btn config-btn';
|
||||
cfgBtn.setAttribute('data-idx', idx);
|
||||
cfgBtn.onclick = function(e) {{ e.stopPropagation(); pushComponent(idx); }};
|
||||
cfgBtn.innerHTML = '配置推送<span class="push-icon" id="cfg-status-' + idx + '"></span>';
|
||||
|
||||
const kpBtn = document.createElement('button');
|
||||
kpBtn.className = 'push-btn kp-btn';
|
||||
kpBtn.setAttribute('data-idx', idx);
|
||||
kpBtn.onclick = function(e) {{ e.stopPropagation(); pushKp(idx); }};
|
||||
kpBtn.innerHTML = '知识推送<span class="push-icon" id="kp-status-' + idx + '"></span>';
|
||||
|
||||
header.insertBefore(kpBtn, regenBtn);
|
||||
header.insertBefore(cfgBtn, kpBtn);
|
||||
}}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
@ -82,30 +82,53 @@ def _load_proxy_config():
|
||||
return None
|
||||
|
||||
|
||||
def _ensure_proxy_running():
|
||||
"""检测代理服务是否运行,未运行则自动启动。返回代理 URL 或 None。"""
|
||||
import requests as _req
|
||||
|
||||
proxy_cfg = _load_proxy_config()
|
||||
if not proxy_cfg:
|
||||
logger.warning("未找到 config.json 或 proxy 配置,跳过代理")
|
||||
return None
|
||||
|
||||
port = proxy_cfg['port']
|
||||
external_ip = proxy_cfg.get('external_ip', '127.0.0.1')
|
||||
health_url = f'http://127.0.0.1:{port}/health'
|
||||
|
||||
# 健康检查
|
||||
def _find_pid_on_port(port):
|
||||
"""查找占用指定端口的进程PID,返回PID或None。"""
|
||||
import signal
|
||||
try:
|
||||
resp = _req.get(health_url, timeout=2)
|
||||
if resp.status_code == 200:
|
||||
logger.info(f"代理服务已运行 (port {port})")
|
||||
return f'http://{external_ip}:{port}/api/push'
|
||||
result = subprocess.run(
|
||||
['ss', '-tlnp', f'sport = :{port}'],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
# 解析 ss 输出中的 pid=XXXX
|
||||
import re as _re
|
||||
m = _re.search(r'pid=(\d+)', result.stdout)
|
||||
if m:
|
||||
return int(m.group(1))
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
# 尝试启动代理
|
||||
logger.info("代理服务未运行,正在启动...")
|
||||
|
||||
def _kill_stale_process(port):
|
||||
"""杀掉占用端口的僵死进程,返回是否成功释放。"""
|
||||
import signal
|
||||
pid = _find_pid_on_port(port)
|
||||
if not pid:
|
||||
return False
|
||||
logger.warning(f"⚠️ 检测到端口 {port} 被僵死进程占用 (PID {pid}),正在终止...")
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
# 等待进程退出
|
||||
for _ in range(5):
|
||||
time.sleep(0.5)
|
||||
try:
|
||||
os.kill(pid, 0) # 检查进程是否还在
|
||||
except OSError:
|
||||
logger.info(f"✓ 僵死进程 (PID {pid}) 已终止")
|
||||
return True
|
||||
# SIGTERM 没杀掉,强制 SIGKILL
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
time.sleep(0.5)
|
||||
logger.info(f"✓ 僵死进程 (PID {pid}) 已强制终止 (SIGKILL)")
|
||||
return True
|
||||
except OSError as e:
|
||||
logger.error(f"终止进程 PID {pid} 失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _start_proxy_process():
|
||||
"""启动代理服务子进程。"""
|
||||
proxy_script = os.path.join(CURRENT_PATH, 'proxy_server.py')
|
||||
subprocess.Popen(
|
||||
[sys.executable, proxy_script],
|
||||
@ -114,18 +137,63 @@ def _ensure_proxy_running():
|
||||
start_new_session=True,
|
||||
)
|
||||
|
||||
# 等待启动
|
||||
for _ in range(5):
|
||||
|
||||
def _ensure_proxy_running():
|
||||
"""检测代理服务是否运行,未运行则自动启动。返回代理 URL 或 None。"""
|
||||
import requests as _req
|
||||
|
||||
proxy_cfg = _load_proxy_config()
|
||||
if not proxy_cfg:
|
||||
logger.warning("⚠️ 未找到 config.json 或 proxy 配置,跳过代理")
|
||||
print("[代理] ⚠️ 未找到代理配置,HTML报告将使用直连地址")
|
||||
return None
|
||||
|
||||
port = proxy_cfg['port']
|
||||
external_ip = proxy_cfg.get('external_ip', '127.0.0.1')
|
||||
health_url = f'http://127.0.0.1:{port}/health'
|
||||
proxy_url = f'http://{external_ip}:{port}/api/push'
|
||||
|
||||
# 健康检查
|
||||
try:
|
||||
resp = _req.get(health_url, timeout=3)
|
||||
if resp.status_code == 200:
|
||||
logger.info(f"✓ 代理服务正常运行 (port {port})")
|
||||
print(f"[代理] ✓ 中转服务正常 → {proxy_url}")
|
||||
return proxy_url
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 健康检查失败 — 检测端口是否被僵死进程占用
|
||||
stale_pid = _find_pid_on_port(port)
|
||||
if stale_pid:
|
||||
logger.warning(f"代理服务健康检查失败,但端口 {port} 被进程 PID {stale_pid} 占用(疑似僵死)")
|
||||
print(f"[代理] ⚠️ 端口 {port} 被僵死进程 (PID {stale_pid}) 占用,正在清理...")
|
||||
if not _kill_stale_process(port):
|
||||
logger.error(f"无法终止僵死进程 PID {stale_pid},HTML报告将使用直连地址")
|
||||
print(f"[代理] ❌ 无法清理僵死进程,HTML报告将使用直连地址")
|
||||
return None
|
||||
time.sleep(1) # 等待端口释放
|
||||
else:
|
||||
logger.info("代理服务未运行,正在启动...")
|
||||
print(f"[代理] 正在启动中转服务 (port {port})...")
|
||||
|
||||
# 启动代理
|
||||
_start_proxy_process()
|
||||
|
||||
# 等待启动(最多8秒)
|
||||
for i in range(8):
|
||||
time.sleep(1)
|
||||
try:
|
||||
resp = _req.get(health_url, timeout=2)
|
||||
if resp.status_code == 200:
|
||||
logger.info(f"代理服务启动成功 (port {port})")
|
||||
return f'http://{external_ip}:{port}/api/push'
|
||||
logger.info(f"✓ 代理服务启动成功 (port {port}, 耗时 {i+1}s)")
|
||||
print(f"[代理] ✓ 中转服务启动成功 → {proxy_url}")
|
||||
return proxy_url
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
logger.warning("代理服务启动失败,HTML报告将使用直连地址")
|
||||
logger.error("❌ 代理服务启动失败,HTML报告将使用直连地址")
|
||||
print("[代理] ❌ 中转服务启动失败!HTML报告将使用直连HTTPS地址(可能受浏览器CORS限制)")
|
||||
return None
|
||||
|
||||
|
||||
@ -290,6 +358,7 @@ def process_script(wiki_url_or_token, db_path=None, dry_run=False):
|
||||
"type_name": type_name,
|
||||
"category": category,
|
||||
"result": result,
|
||||
"raw_config": comp.get("teaching_config", ""),
|
||||
}
|
||||
|
||||
if dry_run:
|
||||
@ -403,6 +472,7 @@ def process_script(wiki_url_or_token, db_path=None, dry_run=False):
|
||||
"status": status,
|
||||
"jsonData": result_data.get("jsonData"),
|
||||
"kpInfo": result_data.get("kpInfo"),
|
||||
"raw_config": res.get("raw_config", ""),
|
||||
}
|
||||
if status == "saved":
|
||||
entry["component_id"] = res.get("component_id")
|
||||
@ -431,6 +501,8 @@ def process_script(wiki_url_or_token, db_path=None, dry_run=False):
|
||||
report["success"] = _progress["success"]
|
||||
report["failed"] = _progress["failed"]
|
||||
report["skipped"] = _progress["skipped"]
|
||||
report["character_map"] = character_map
|
||||
report["level"] = level
|
||||
|
||||
# 记录日志路径到报告
|
||||
report["log_path"] = log_path
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
"""轻量 CORS 代理服务 - 转发推送请求到目标 API"""
|
||||
"""轻量 CORS 代理服务 - 转发推送请求到目标 API + 单组件重新生成"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import threading
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
import traceback
|
||||
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
SCRIPTS_DIR = Path(__file__).resolve().parent
|
||||
LOG_DIR = PROJECT_ROOT / 'logs'
|
||||
LOG_DIR.mkdir(exist_ok=True)
|
||||
|
||||
@ -56,6 +59,42 @@ class RateLimiter:
|
||||
rate_limiter = RateLimiter(CONFIG['rate_limit_per_minute'])
|
||||
|
||||
|
||||
# --- Regenerate Handler ---
|
||||
def _do_regenerate(body_data):
|
||||
"""调用 generate_component 重新生成单个组件"""
|
||||
if str(SCRIPTS_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||
|
||||
from generate_json import generate_component
|
||||
from llm_client import LLMClient
|
||||
|
||||
teaching_config = body_data.get("teaching_config", "")
|
||||
type_name = body_data.get("type_name", "")
|
||||
cId = body_data.get("cId", "")
|
||||
character_map = body_data.get("character_map") or {}
|
||||
level = body_data.get("level") or None
|
||||
|
||||
if not teaching_config or not type_name:
|
||||
return 400, {"error": "缺少 teaching_config 或 type_name"}
|
||||
|
||||
component = {
|
||||
"type_name": type_name,
|
||||
"cId": cId,
|
||||
"teaching_config": teaching_config,
|
||||
"has_image": body_data.get("has_image", False),
|
||||
"knowledge_text": body_data.get("knowledge_text", ""),
|
||||
"config_info": body_data.get("config_info", ""),
|
||||
}
|
||||
|
||||
try:
|
||||
llm_client = LLMClient()
|
||||
result = generate_component(component, character_map=character_map, llm_client=llm_client, level=level)
|
||||
return 200, result
|
||||
except Exception as e:
|
||||
logger.error(f"Regenerate failed: {traceback.format_exc()}")
|
||||
return 500, {"error": str(e)}
|
||||
|
||||
|
||||
class ProxyHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def _send_json(self, code, data, extra_headers=None):
|
||||
@ -79,7 +118,7 @@ class ProxyHandler(BaseHTTPRequestHandler):
|
||||
return self.client_address[0]
|
||||
|
||||
def do_OPTIONS(self):
|
||||
if self.path == '/api/push':
|
||||
if self.path in ('/api/push', '/api/regenerate'):
|
||||
self.send_response(204)
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS')
|
||||
@ -96,10 +135,26 @@ class ProxyHandler(BaseHTTPRequestHandler):
|
||||
self.send_error(404)
|
||||
|
||||
def do_POST(self):
|
||||
if self.path != '/api/push':
|
||||
if self.path == '/api/push':
|
||||
self._handle_push()
|
||||
elif self.path == '/api/regenerate':
|
||||
self._handle_regenerate()
|
||||
else:
|
||||
self.send_error(404)
|
||||
return
|
||||
|
||||
def _read_body(self, max_size=2 * 1024 * 1024):
|
||||
content_length = int(self.headers.get('Content-Length', 0))
|
||||
if content_length == 0:
|
||||
return None, (400, {'error': 'empty body'})
|
||||
if content_length > max_size:
|
||||
return None, (413, {'error': 'body too large'})
|
||||
raw_body = self.rfile.read(content_length)
|
||||
try:
|
||||
return json.loads(raw_body), None
|
||||
except json.JSONDecodeError:
|
||||
return None, (400, {'error': 'invalid JSON'})
|
||||
|
||||
def _handle_push(self):
|
||||
client_ip = self._get_client_ip()
|
||||
if not rate_limiter.is_allowed(client_ip):
|
||||
self._send_json(429, {'error': 'rate limit exceeded'})
|
||||
@ -147,13 +202,32 @@ class ProxyHandler(BaseHTTPRequestHandler):
|
||||
self._send_json(502, {'error': f'upstream error: {str(e)}'})
|
||||
logger.error(f'[{client_ip}] Forward failed: {e}')
|
||||
|
||||
def _handle_regenerate(self):
|
||||
client_ip = self._get_client_ip()
|
||||
logger.info(f'[{client_ip}] POST /api/regenerate')
|
||||
|
||||
body_data, err = self._read_body()
|
||||
if err:
|
||||
self._send_json(err[0], err[1])
|
||||
return
|
||||
|
||||
if not isinstance(body_data, dict) or not body_data.get('teaching_config'):
|
||||
self._send_json(400, {'error': 'body must contain teaching_config'})
|
||||
return
|
||||
|
||||
logger.info(f'[{client_ip}] Regenerating: type_name={body_data.get("type_name")}, cId={body_data.get("cId")}')
|
||||
|
||||
code, result = _do_regenerate(body_data)
|
||||
self._send_json(code, result)
|
||||
logger.info(f'[{client_ip}] Regenerate done: status={code}')
|
||||
|
||||
def log_message(self, format, *args):
|
||||
logger.debug(f'{self.client_address[0]} - {format % args}')
|
||||
|
||||
|
||||
def main():
|
||||
port = CONFIG['port']
|
||||
server = HTTPServer(('0.0.0.0', port), ProxyHandler)
|
||||
server = ThreadingHTTPServer(('0.0.0.0', port), ProxyHandler)
|
||||
logger.info(f'Proxy server started on 0.0.0.0:{port}')
|
||||
logger.info(f'Target: {CONFIG["target_url"]}')
|
||||
logger.info(f'Rate limit: {CONFIG["rate_limit_per_minute"]} req/min per IP')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user