工作台:目录改名 mcn-workshop + 任务进度可见 + 会话归属配置化
1. 目录 mcn-work-shop -> mcn-workshop(空间分组名取会话 cwd 的目录名,只改字符串无效,必须真改名) 2. 全仓替换 mcn-work-shop -> mcn-workshop:37 文件 121 处;历史日志按沿革句规矩保留当时目录名 3. 任务进度可见:/api/run/status 产出 已运行时长/工具调用/最近动作/停滞判定(读会话日志尾部),前端新增右下角常驻面板,四处任务入口接入 4. 会话归属目录配置化:新增 config.sessionCwd(留空=工作台自身目录),现指向 D://AI技能//mcn-workshop,使任务显示为命名分组而非「未分组任务」 5. 修前端轮询静默缺陷:连续 3 次查询失败即提示服务断开(原逻辑静默空转到 15 分钟超时,用户零感知)
This commit is contained in:
1 parent
d07049ecf1
commit
f77735b206
115 files changed
+1103
-1514
No files matched your search
@@ -64,21 +64,21 @@ agent_created: true
|
||||
|
||||
用户说「打开工作台 / 打开MCN工作台」时:
|
||||
|
||||
1. 后台启动本地服务:`node <本技能目录>/mcn-work-shop/server.js`(零依赖 Node,无需安装包;Windows 也可双击 `start.bat`)
|
||||
1. 后台启动本地服务:`node <本技能目录>/mcn-workshop/server.js`(零依赖 Node,无需安装包;Windows 也可双击 `start.bat`)
|
||||
2. 用内置浏览器打开 `http://localhost:8900`。**注意端口**:本机 8899 可能被 `MiPCAudio.exe`(小米 PC 管家)占用系统级 0.0.0.0:8899 且杀后自愈,工作台默认端口已迁移为 **8900**;如需指定端口用 `node server.js <端口>`(`PORT_BASE = parseInt(process.argv[2],10) || 8900`)
|
||||
3. 工作台采用 hash 路由,页面结构:
|
||||
- **首页(#/)**:数据功能卡片(抖音周榜/热门账号/AI写脚本/AI写作复盘)+ 5 大技能功能块(问法点击复制)+ 三种创作模式
|
||||
- **📈 抖音周榜(#/ranking)**:读桌面 `MCNSkill项目\三方数据\红狐数据\抖音榜单\*.json`(运行时解析真实桌面、兼容 OneDrive 重定向;环境变量 `MCN_RANKING_DIR` 可覆盖),赛道筛选 + 日期切换 + 数值排序
|
||||
- **👥 热门账号(#/accounts)**:读工作台自有数据库副本 `mcn-work-shop/mcn-plugin.db`(只读),分页/搜索/赛道/类型筛选,账号点击进详情
|
||||
- **👥 热门账号(#/accounts)**:读工作台自有数据库副本 `mcn-workshop/mcn-plugin.db`(只读),分页/搜索/赛道/类型筛选,账号点击进详情
|
||||
- **账号详情(#/account/:id)**:信息卡 + 账号信息/账号设定/数据分析 3 tab + 视频列表分页;解析/改写/提炼按钮直接提交 AI 任务执行
|
||||
- **视频详情(#/video/:id)**:信息卡 + 视频脚本/视频拆解/视频分析 3 tab
|
||||
- **✍️ AI写脚本(#/rewrites)**:爆款视频列表 + 源脚本 vs AI脚本双栏对比 + 分镜提示词分块渲染
|
||||
- **🧪 AI写作复盘(#/reviews)**:评分维度列表 + 复盘详情(评分条/问题清单/报告)
|
||||
- **📁 产出浏览(#/files)**:默认读桌面 `MCNSkill项目/`,支持 ⚙️ 设置添加其他项目地址;md/json 在线预览
|
||||
4. **数据边界**:数据库读工作台自有副本 `mcn-work-shop/mcn-plugin.db`(**dsh 侧已整套下线(09-10):`~/.dsh/mcn-plugin.db` 与 `D:\dshworkspace` 均不存在**,工作台 DB 为唯一读写副本);周榜读桌面 `MCNSkill项目\三方数据\红狐数据\抖音榜单\*.json` 文件
|
||||
5. **AI 任务触发(Automation 方案)**:各数据页「导入/创作/解析/改写/提炼/复盘」按钮点击后 → 工作台后端 `POST /api/run` → 向 `~/.workbuddy/workbuddy.db` 的 `automations` 表写入一条**一次性任务**(`schedule_type='once'`、**`scheduled_at=now+5s`(带秒)且显式写 `next_run_at=now+5000`**、`cwds=[<工作台目录>]`(由 `server.js` 从 `ROOT` 动态推导,值 = `<技能根>/mcn-work-shop`,**禁止写死盘符**))→ WorkBuddy 客户端调度器按 `next_run_at` 扫描拾取(周期≤30s)→ 创建 AI 会话执行 → **会话归入 `mcn-work-shop` 空间分组**(左侧会话栏按 cwd 分组,可在该分组查看任务执行过程与结果)。参考 dsh 插件 `dsh-plugin-mcn` 的 `/mcn/api/creative` 模式(固定会话 followup 触发);前端 5 秒防连点。**调度机制/状态机/并发上限/排查命令详见同目录 `自动化任务调度机制.md`(09-01 实测定稿,勿再按旧认知 `scheduled_at=now` 直写——会因 next_run_at 为空而卡死)**
|
||||
4. **数据边界**:数据库读工作台自有副本 `mcn-workshop/mcn-plugin.db`(**dsh 侧已整套下线(09-10):`~/.dsh/mcn-plugin.db` 与 `D:\dshworkspace` 均不存在**,工作台 DB 为唯一读写副本);周榜读桌面 `MCNSkill项目\三方数据\红狐数据\抖音榜单\*.json` 文件
|
||||
5. **AI 任务触发(Automation 方案)**:各数据页「导入/创作/解析/改写/提炼/复盘」按钮点击后 → 工作台后端 `POST /api/run` → 向 `~/.workbuddy/workbuddy.db` 的 `automations` 表写入一条**一次性任务**(`schedule_type='once'`、**`scheduled_at=now+5s`(带秒)且显式写 `next_run_at=now+5000`**、`cwds=[<工作台目录>]`(由 `server.js` 从 `ROOT` 动态推导,值 = `<技能根>/mcn-workshop`,**禁止写死盘符**))→ WorkBuddy 客户端调度器按 `next_run_at` 扫描拾取(周期≤30s)→ 创建 AI 会话执行 → **会话归入 `mcn-workshop` 空间分组**(左侧会话栏按 cwd 分组,可在该分组查看任务执行过程与结果)。参考 dsh 插件 `dsh-plugin-mcn` 的 `/mcn/api/creative` 模式(固定会话 followup 触发);前端 5 秒防连点。**调度机制/状态机/并发上限/排查命令详见同目录 `自动化任务调度机制.md`(09-01 实测定稿,勿再按旧认知 `scheduled_at=now` 直写——会因 next_run_at 为空而卡死)**
|
||||
6. **工作台任务执行方式铁律(P0,09-02 固化)**:凡工作台场景的**创作/改写/重写/复盘/解析**类任务(含"重新执行历史任务"),必须通过 `POST /api/run` 提交 AI 会话任务执行(automation 触发 → 独立 AI 会话按技能 S1-S11 执行 → 写库 rewrite_log/入库),**禁止在会话内直接产出脚本草稿**(不会入库、不在左侧会话栏可见、不走技能会话,等于"影子脚本")。**重新执行历史任务 = 复用原任务的 prompt(人设卡+视频选题+创作要求+落库字段)+ `skills:['短视频工作台']` 提交新任务**——新任务自动加载当前技能版本(含最新优化),无需手工搬内容。执行方式判定细则见「关键规则 〇·执行方式判定」。
|
||||
7. **AI 任务入口技能契约(P0,09-02 固化)**:工作台每个触发 AI 会话任务的入口都必须遵守「功能入口 × 技能调用契约」——任务 prompt = **固定技能声明前缀常量**(SKILL_HINT_CREATE/EXTRACT/STORYBOARD/REVIEW/ACCOUNT,分别对应 创作/提炼/分镜/复盘/账号抓取)+ **业务正文**,并传对应 `skills` 数组(参数级技能挂载)。**新增/修改任何入口时先读 `mcn-work-shop/docs/AI会话任务输入输出对照.md`(契约总表 + 新增入口检查清单 7 项),再动手**——防裸 prompt 跳步(09-02 三入口缺约束根因)、防技能声明混入业务串、防非抓取任务命中 server.js BROWSER_HINT 正则被误注入浏览器锁。
|
||||
7. **AI 任务入口技能契约(P0,09-02 固化)**:工作台每个触发 AI 会话任务的入口都必须遵守「功能入口 × 技能调用契约」——任务 prompt = **固定技能声明前缀常量**(SKILL_HINT_CREATE/EXTRACT/STORYBOARD/REVIEW/ACCOUNT,分别对应 创作/提炼/分镜/复盘/账号抓取)+ **业务正文**,并传对应 `skills` 数组(参数级技能挂载)。**新增/修改任何入口时先读 `mcn-workshop/docs/AI会话任务输入输出对照.md`(契约总表 + 新增入口检查清单 7 项),再动手**——防裸 prompt 跳步(09-02 三入口缺约束根因)、防技能声明混入业务串、防非抓取任务命中 server.js BROWSER_HINT 正则被误注入浏览器锁。
|
||||
8. 本功能仅 V1.0 源技能环境提供,**不同步 dsh/Lite 副本**
|
||||
9. **改完必重开页面(硬性约定)**:对工作台任何页面/代码改动完成后,**最后一步必须用 present_files 重新打开对应页面**(带 hash 直接定位,如 `http://localhost:8900/#/accounts`),让用户在 WorkBuddy 内置浏览器立即看到最新效果。原因:内置浏览器无 CDP 调试端口(9222/9224 等均无响应),无法模拟 F5;重开页面是唯一可靠刷新方式。此约定对工作台所有 UI 改动强制生效
|
||||
|
||||
@@ -150,7 +150,7 @@ agent_created: true
|
||||
- **技能本体**:`subskills/lieflat-charts/`(相对本文件;含 SKILL.md + catalog.md + templates/ 各 gallery,随源仓库分发)
|
||||
- **选型规则**:先读 `catalog.md` 按数据形状锁定图型编号,再打开 `templates/` 对应 gallery 的真实实现复制骨架;默认顺序 Lupi Editorial → Lupi Basics → Glance(工作台场景数据量小/需快读时可直用 Glance)
|
||||
- **视觉 token**:`mono-tokens.js`(纸 #F0EFEB / 墨 #1C1C1A / 灰 #8F8E88 / 网格 #DEDDD6),同一交付只锁一种色系
|
||||
- **ECharts 依赖**:已本地化至工作台 `mcn-work-shop/public/vendor/echarts.min.js`(零 CDN 依赖,离线可用);引用样例见工作台账号详情页视频列表上方「点赞/评论趋势折线图」(G8 Rainfall 骨架:双 grid、双序列随时间、tipLight tooltip、quarticOut 入场)
|
||||
- **ECharts 依赖**:已本地化至工作台 `mcn-workshop/public/vendor/echarts.min.js`(零 CDN 依赖,离线可用);引用样例见工作台账号详情页视频列表上方「点赞/评论趋势折线图」(G8 Rainfall 骨架:双 grid、双序列随时间、tipLight tooltip、quarticOut 入场)
|
||||
- **使用限制**:库外新造是最后手段(须继承最接近 gallery 模板的视觉语法);不得拼接多个模板造混合图型
|
||||
|
||||
---
|
||||
@@ -338,7 +338,7 @@ S1 意图识别 → S2 需求完善 → [G0 分析确认]
|
||||
|
||||
> 判定口诀:**"这个产物要不要进工作台(rewrite_log/入库/左侧会话栏)?"**——要进,就走任务;不进,才可会话内产出。
|
||||
> 重新执行历史任务 = 复用原任务 prompt(从 automations 表取)+ `skills` 参数提交新任务,任务自动应用当前技能版本。
|
||||
> **组装 prompt/选择 skills 时遵守「功能入口 × 技能调用契约」**(工作台章节第 7 条 + `mcn-work-shop/docs/AI会话任务输入输出对照.md`)——任务必须带技能前缀常量(SKILL_HINT_*)+ 对应 `skills` 数组。
|
||||
> **组装 prompt/选择 skills 时遵守「功能入口 × 技能调用契约」**(工作台章节第 7 条 + `mcn-workshop/docs/AI会话任务输入输出对照.md`)——任务必须带技能前缀常量(SKILL_HINT_*)+ 对应 `skills` 数组。
|
||||
|
||||
### 〇·1、方案与执行分离(P0)
|
||||
|
||||
@@ -397,7 +397,7 @@ Skill部署到非原始开发路径时,所有用户新增的创作流程/接
|
||||
├── SKILL.md ← 入口文件(流程引擎+规则摘要+子技能索引+目录索引)
|
||||
├── 帮助文档.md ← 使用指南+触发示例+快速启动模板
|
||||
├── scripts/ ← MCP配置 + Dify接口脚本
|
||||
├── mcn-work-shop/ ← 本地 Web 工作台(server.js + public/,零依赖 Node)
|
||||
├── mcn-workshop/ ← 本地 Web 工作台(server.js + public/,零依赖 Node)
|
||||
├── subskills/ ← 6 个子技能(browser-harness / lieflat-charts / mcn-data-insight / mcn-dou-analysis / mcn-script-review / mcn-video-prompt,相对路径引用见「子技能索引」)
|
||||
├── references/ ← 标准库
|
||||
│ ├── 创作流程规范.md ← 确认门/格式/路径/硬编码 规则权威源
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
# AI 链路:本地 CodeBuddy CLI 接入(A 方案)
|
||||
|
||||
> 2026-09-29 落地。工作台的 AI 能力(需求打磨 / 创作)除 Dify 外,新增**本地 CodeBuddy CLI** 通路。
|
||||
> 实现文件:`cli-backend.js`(后端)+ `server.js` 的 `/api/ai/clarify`、`/api/ai/status`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 一句话
|
||||
|
||||
把「工作台 → 模型」从 **HTTP 调 Dify** 换成 **spawn 一个 `codebuddy` 子进程、用 stdio 通信**。
|
||||
不涉及任何本地端口,与 WorkBuddy 桌面程序完全解耦。
|
||||
|
||||
---
|
||||
|
||||
## 2. 为什么不是「直接用宿主那个 CLI」
|
||||
|
||||
排查中确认了三条硬结论,决定了必须自备凭据:
|
||||
|
||||
| 结论 | 证据 |
|
||||
|:--|:--|
|
||||
| 宿主 gateway 端口/密码**每轮轮换**,且无处落盘 | 同机同天实测 `12134 → 13116 → 10261`;`D:/.workbuddy/` 下无 worker/registry 文件 |
|
||||
| 唯一发现通道是环境变量,且**只存在于 WorkBuddy 派生的进程树内** | `CODEBUDDY_SERVICE_PROXY_URL=http://127.0.0.1:<port>/...`,用户双击 `start.bat` 起的工作台拿不到 |
|
||||
| WorkBuddy **故意**不让子进程继承凭据 | 用 4 个环境变量把凭据交给 CLI,CLI 读完**立即 `delete`**;凭据不落明文盘 |
|
||||
|
||||
→ 所以走官方文档给的非交互认证通道:`CODEBUDDY_API_KEY` / `CODEBUDDY_AUTH_TOKEN`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 配置步骤
|
||||
|
||||
### 步骤 1:拿 API Key
|
||||
|
||||
| 版本 | 地址 |
|
||||
|:--|:--|
|
||||
| **中国版**(本机是这版,`CODEBUDDY_INTERNET_ENVIRONMENT=internal`) | https://copilot.tencent.com/profile/ |
|
||||
| 国际版 | https://www.codebuddy.ai/profile/keys |
|
||||
| iOA | https://tencent.sso.copilot.tencent.com/profile/keys |
|
||||
|
||||
### 步骤 2:填进 `config.json`
|
||||
|
||||
```json
|
||||
"ai": {
|
||||
"backend": "cli",
|
||||
"cli": {
|
||||
"path": "",
|
||||
"model": "custom:doubao-seed-2-1-pro-260628",
|
||||
"apiKey": "<你的 API Key>",
|
||||
"authToken": "",
|
||||
"internetEnvironment": "internal",
|
||||
"permissionMode": "",
|
||||
"timeoutMs": 300000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
只改两个值即可:`backend` → `"cli"`,`apiKey` → 你的 key。
|
||||
|
||||
### 步骤 3:自检
|
||||
|
||||
```bash
|
||||
# 只报告配置(零消耗)
|
||||
curl "http://localhost:8900/api/ai/status"
|
||||
|
||||
# 真跑一次极短提示,验证端到端(消耗一次调用)
|
||||
curl "http://localhost:8900/api/ai/status?probe=1"
|
||||
```
|
||||
|
||||
期望看到 `"ready": true` 且 `probe.ok = true`。若 `ready: false` → key 没填对;
|
||||
若 `probe.error` 里出现 `Authentication required` → key 无效或 `internetEnvironment` 版本错了。
|
||||
|
||||
---
|
||||
|
||||
## 4. 字段说明
|
||||
|
||||
| 字段 | 说明 |
|
||||
|:--|:--|
|
||||
| `ai.backend` | `"cli"` 走本地 CLI;`"dify"` 走原链路。可用环境变量 `MCN_AI_BACKEND` 临时覆盖(不改文件、不重启) |
|
||||
| `ai.cli.path` | 留空 → 自动探测:`CODEBUDDY_CLI_PATH` 环境变量 → WorkBuddy 自带 CLI → `PATH` 里的 `codebuddy` |
|
||||
| `ai.cli.model` | 模型 ID。可用值见 `codebuddy --model` 帮助(含 `custom:doubao-seed-2-1-pro-260628`) |
|
||||
| `ai.cli.apiKey` | 留空 → 读环境变量 `CODEBUDDY_API_KEY` |
|
||||
| `ai.cli.authToken` | 留空 → 读环境变量 `CODEBUDDY_AUTH_TOKEN`。**优先级最高**(官方:`AUTH_TOKEN` > `apiKeyHelper` > `API_KEY`) |
|
||||
| `ai.cli.internetEnvironment` | 中国版 `internal`;iOA `ioa`;国际版留空 |
|
||||
| `ai.cli.timeoutMs` | 单次调用超时,默认 300000(5 分钟) |
|
||||
|
||||
---
|
||||
|
||||
## 5. 实现里必须保留的三条防护(改动前务必读)
|
||||
|
||||
`cli-backend.js` 的 `buildChildEnv()` 做了三件事,**每一条都是实测踩出来的坑**:
|
||||
|
||||
### 5.1 删掉 `SERVER__PORT` / `SERVER__HOST`
|
||||
|
||||
`codebuddy`(**连 `-p` 打印模式也**)内部会起一个 HTTP server:
|
||||
|
||||
```js
|
||||
listen(parseInt(process.env.SERVER__PORT) || config.get("cell.server", { port: 3000 }).port)
|
||||
```
|
||||
|
||||
在 WorkBuddy 进程树内启动时,会继承宿主给自己设的 `SERVER__PORT` → 去 listen 宿主已占用的端口 →
|
||||
`EADDRINUSE` → **进程静默卡死**(HTTP 层看似正常,只有 agent 执行环节挂掉,极具迷惑性)。
|
||||
|
||||
官方自己在派生子进程时也是这么做的:`delete el.SERVER__PORT, delete el.SERVER__HOST`。
|
||||
|
||||
### 5.2 强制 `CODEBUDDY_FORCE_HEADLESS_BUNDLE=1`
|
||||
|
||||
`bin/codebuddy` 这个 launcher 按环境变量选 bundle:
|
||||
|
||||
| 环境变量 | 命中产物 |
|
||||
|:--|:--|
|
||||
| `CODEBUDDY_FORCE_LITE_WB_BUNDLE=1`(宿主注入,**优先级最高**) | `codebuddy-lite-wb.mjs`(WorkBuddy 特供精简包) |
|
||||
| `CODEBUDDY_FORCE_HEADLESS_BUNDLE=1` | `codebuddy-headless.js`(官方 headless,**我们要的**) |
|
||||
| 含 `--print` | 自动 headless |
|
||||
| 都没有 | `dist/codebuddy`(**本安装里不存在** → `MODULE_NOT_FOUND`) |
|
||||
|
||||
不显式清除 wb 特供包分流,会误用给宿主定制的产物。
|
||||
|
||||
### 5.3 清掉 `NODE_OPTIONS` 与会话上下文变量
|
||||
|
||||
宿主会注入 `NODE_OPTIONS=--require=...node-language-shim.cjs`、`CODEBUDDY_SESSION_*`、
|
||||
`CODEBUDDY_SERVICE_PROXY_URL`、`BAGGAGE` 等,子进程继承会串味。一并清掉,并把
|
||||
`CODEBUDDY_CONFIG_DIR` 指向独立目录(默认 `<tmpdir>/mcn-cli-home`),避免污染 WorkBuddy 的配置与会话历史。
|
||||
|
||||
---
|
||||
|
||||
## 6. 输出协议
|
||||
|
||||
工作台对前端只暴露一套 SSE 协议,`cli` 与 `dify` 两条后端产出完全一致,**前端无需感知差异**:
|
||||
|
||||
| 事件 | 含义 |
|
||||
|:--|:--|
|
||||
| `{"delta": "..."}` | 文本增量,直接追加渲染 |
|
||||
| `{"replace": "..."}` | 整体覆盖(终态校准 / 出错但已有产出) |
|
||||
| `{"error": "..."}` | 错误(附带可操作的中文提示) |
|
||||
| `data: [DONE]` | 流结束 |
|
||||
|
||||
CLI 侧的事件解析(`--output-format stream-json --include-partial-messages`):
|
||||
|
||||
| CLI 事件 | 处理 |
|
||||
|:--|:--|
|
||||
| `type:"system", subtype:"init"` | 取 `session_id` |
|
||||
| `type:"stream_event"` → `event.content_block_delta` → `delta.type==="text_delta"` | 取 `delta.text` 作为增量 |
|
||||
| `delta.type==="thinking_delta"` | 思考增量(可选回调) |
|
||||
| `type:"assistant"` | 未产生增量流时的整段文本兜底 |
|
||||
| `type:"result"` | 终态。`is_error:true` 时真实原因在 **`errors[]`**,正文作废 |
|
||||
|
||||
> ⚠️ 未认证时,CLI 会把报错文本**当成 assistant 文本**发出来。
|
||||
> `isCliBoilerplate()` 负责识别并拦截,避免把英文报错显示成"AI 的回答"。
|
||||
|
||||
---
|
||||
|
||||
## 7. 排障速查
|
||||
|
||||
| 现象 | 原因 | 处理 |
|
||||
|:--|:--|:--|
|
||||
| 进程卡死无输出 | `SERVER__PORT` 未清除 → EADDRINUSE | 确认 `buildChildEnv` 的 delete 生效 |
|
||||
| `Cannot find module '../dist/codebuddy'` | bundle 分流未命中 | 设 `CODEBUDDY_FORCE_HEADLESS_BUNDLE=1` |
|
||||
| `Authentication required. Please use /login` | 无凭据 | 按 §3 填 key |
|
||||
| `ready:false` 但 key 已填 | `internetEnvironment` 版本不对 | 中国版必须是 `internal` |
|
||||
| HTTP 层全正常、agent 却不干活 | 典型的 EADDRINUSE 假象 | 看 §5.1 |
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,9 @@
|
||||
# 2026-10-08 工作日志
|
||||
|
||||
## 榜单数据更新(自动化 automation-1791425215326)
|
||||
|
||||
- 按 mcn-data-insight「榜单数据落盘规范」抓取热点数据四榜最新一期,落盘目录:`C:\Users\maidou\Desktop\MCNSkill项目\三方数据\红狐数据\抖音榜单\`
|
||||
- 写入 8 个文件:账号日榜 1(数据期 2026-10-06)、账号周榜 5 赛道(数据期 2026-09-28~10-04)、视频热榜 1(周标号 2026-09-28)、点赞榜 1(周标号 2026-09-28),各 50 条
|
||||
- 实际 API 调用 8 次(hot1 + likes1 + 日榜1 + 周榜5),无缓存命中(上一期:日榜 09-26、周榜 09-07、视频/点赞 09-21)
|
||||
- 运行要点:`fetch_week_ranks.py` 需 cd 到 scripts 目录用 `D:/miniconda3/python.exe` 跑;`fetch_rank.py` 先落临时目录再按 JSON 内 dateStart 重命名到正式文件名
|
||||
- 注意:Bash `mktemp` 得到的是 /tmp 路径,传给 Python 前需 `cygpath -w` 转 Windows 路径,否则 FileNotFoundError
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+13
@@ -0,0 +1,13 @@
|
||||
# automation-1790682160345 执行记录
|
||||
|
||||
## 任务性质
|
||||
链路自检(健康检查)任务。指令:仅回复「链路正常」四字。
|
||||
|
||||
## 历史执行
|
||||
| 时间 | 结果 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 2026-09-29 19:43 | 成功 | 定时任务正常触发并返回,链路通畅。首次执行,无历史记录文件,已初始化本文件。 |
|
||||
|
||||
## 备注
|
||||
- 该任务不产出任何交付文件,无需 present_files。
|
||||
- 只做链路连通性验证:定时调度 → 会话唤起 → 模型响应 全链路可达。
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# automation-1791425215326 · 榜单数据更新(热点数据四榜落盘)
|
||||
|
||||
## 执行约定(复用)
|
||||
- 落盘目录:`C:\Users\maidou\Desktop\MCNSkill项目\三方数据\红狐数据\抖音榜单\`(桌面未 OneDrive 重定向;env `MCN_RANKING_DIR` 可覆盖)
|
||||
- 四榜:账号日榜(全品类 1 次)/账号周榜(5 赛道:生活vlog 小剧场 亲子 美食 旅行)/视频热榜/点赞榜(各 1 次,不传赛道)
|
||||
- 幂等:调用前先 ls 目标子目录,同榜名+赛道+数据日期已存在则跳过
|
||||
- Python 用 `D:/miniconda3/python.exe`;`fetch_week_ranks.py` 在 scripts 目录执行;`fetch_rank.py` 先写临时目录,按 JSON 内 dateStart 定正式文件名
|
||||
|
||||
## 执行历史
|
||||
| 运行日期 | 数据期 | 写入 | API 调用 | 备注 |
|
||||
|---|---|---|---|---|
|
||||
| 2026-10-08 | 日榜 2026-10-06;周榜/热榜/点赞 2026-09-28 | 8 个文件(各 50 条) | 8 次 | 该自动化首次记录执行;上一期缓存为日榜 09-26 / 周榜 09-07 / 视频·点赞 09-21,全部未命中故全量抓取 |
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# automation-1791433363165 执行记录
|
||||
|
||||
## 2026-10-08 12:23
|
||||
- 任务:按用户要求只回复两个字「成功」。
|
||||
- 结果:已按字面要求输出「成功」,无文件产出。
|
||||
- 备注:本次为首次执行,无历史记录。
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+119
-12
@@ -69,6 +69,50 @@ function resolveCli(explicitPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 凭据解析
|
||||
|
||||
// 凭据默认落点:**刻意放在仓库之外**。
|
||||
// config.json 是 git 跟踪文件,绝不能让 key 落进去;本目录也不会被技能同步脚本复制。
|
||||
function defaultCredentialFile() {
|
||||
return path.join(os.homedir(), '.workbuddy', 'mcn-workshop', 'ai-cli.json');
|
||||
}
|
||||
|
||||
// 读凭据文件:{"apiKey":"...","authToken":"..."}
|
||||
function readCredentialFile(p) {
|
||||
if (!p) return {};
|
||||
try {
|
||||
if (!fs.existsSync(p)) return {};
|
||||
const j = JSON.parse(fs.readFileSync(p, 'utf8'));
|
||||
return {
|
||||
apiKey: typeof j.apiKey === 'string' ? j.apiKey.trim() : '',
|
||||
authToken: typeof j.authToken === 'string' ? j.authToken.trim() : '',
|
||||
file: p,
|
||||
};
|
||||
} catch (e) { return {}; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 凭据优先级(高 → 低):
|
||||
* 1) 调用方显式传入(config.json 的 ai.cli.apiKey / authToken)
|
||||
* 2) 凭据文件(默认 ~/.workbuddy/mcn-workshop/ai-cli.json)
|
||||
* 3) 环境变量 CODEBUDDY_API_KEY / CODEBUDDY_AUTH_TOKEN
|
||||
* 返回 { apiKey, authToken, source }
|
||||
*/
|
||||
function resolveCredentials(o) {
|
||||
o = o || {};
|
||||
const f = readCredentialFile(o.credentialFile || defaultCredentialFile());
|
||||
const apiKey = o.apiKey || f.apiKey || process.env.CODEBUDDY_API_KEY || '';
|
||||
const authToken = o.authToken || f.authToken || process.env.CODEBUDDY_AUTH_TOKEN || '';
|
||||
let source = '(未配置)';
|
||||
if (o.authToken) source = 'config:authToken';
|
||||
else if (o.apiKey) source = 'config:apiKey';
|
||||
else if (f.authToken) source = 'file:authToken ' + f.file;
|
||||
else if (f.apiKey) source = 'file:apiKey ' + f.file;
|
||||
else if (process.env.CODEBUDDY_AUTH_TOKEN) source = 'env:CODEBUDDY_AUTH_TOKEN';
|
||||
else if (process.env.CODEBUDDY_API_KEY) source = 'env:CODEBUDDY_API_KEY';
|
||||
return { apiKey, authToken, source, credentialFile: f.file || defaultCredentialFile() };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 环境构造
|
||||
|
||||
// 把「宿主 WorkBuddy 注入的、会干扰子 CLI 的」环境变量剔除,并注入凭据
|
||||
@@ -86,6 +130,9 @@ function buildChildEnv(opts) {
|
||||
for (const k of Object.keys(env)) {
|
||||
if (k.startsWith('CODEBUDDY_SESSION_') || k.startsWith('CODEBUDDY_CONVERSATION_') ||
|
||||
k === 'CODEBUDDY_TOOL_CALL_ID' || k === 'CODEBUDDY_SERVICE_PROXY_URL' ||
|
||||
k === 'CODEBUDDY_MCP_CONFIG' || // 宿主 MCP(weixinpay/sheetagent…)我们不要
|
||||
k === 'CODEBUDDY_GATEWAY_PASSWORD' || k === 'CODEBUDDY_GATEWAY_AUTH' ||
|
||||
k === 'WORKBUDDY_PAC_RPC_TOKEN' || k === 'WORKBUDDY_PAC_RPC_SOCKET' ||
|
||||
k === 'SANDBOX_CENTER_IPC_ADDRESS' || k === 'BAGGAGE' || k === 'BASH_ENV') {
|
||||
delete env[k];
|
||||
}
|
||||
@@ -97,12 +144,17 @@ function buildChildEnv(opts) {
|
||||
env.CODEBUDDY_SAFE_DELETE_ENABLED = '0'; // 工作台场景不需要工具删除保护
|
||||
env.CODEBUDDY_GATEWAY_DISABLE_API_DOCS = '1';
|
||||
env.CODEBUDDY_DISABLE_TELEMETRY = '1';
|
||||
// 官方建议:纯 -p 单发不支持后台任务,开着会返回明确错误
|
||||
env.CODEBUDDY_CODE_DISABLE_BACKGROUND_TASKS = '1';
|
||||
|
||||
// 【坑 3】凭据:独立进程必须自备
|
||||
const key = opts.apiKey || process.env.CODEBUDDY_API_KEY || '';
|
||||
const token = opts.authToken || process.env.CODEBUDDY_AUTH_TOKEN || '';
|
||||
if (token) env.CODEBUDDY_AUTH_TOKEN = token; // 优先级最高
|
||||
if (key) env.CODEBUDDY_API_KEY = key;
|
||||
// 【坑 3】凭据:独立进程必须自备。
|
||||
// 注意:宿主也会用同名变量注入凭据且读后即删——必须先把继承来的残留清掉,再写我们自己的,
|
||||
// 否则可能带着一个「空壳/过期」的值进入子进程。
|
||||
delete env.CODEBUDDY_API_KEY;
|
||||
delete env.CODEBUDDY_AUTH_TOKEN;
|
||||
const cred = resolveCredentials(opts);
|
||||
if (cred.authToken) env.CODEBUDDY_AUTH_TOKEN = cred.authToken; // 优先级最高
|
||||
if (cred.apiKey) env.CODEBUDDY_API_KEY = cred.apiKey;
|
||||
if (opts.internetEnvironment) env.CODEBUDDY_INTERNET_ENVIRONMENT = opts.internetEnvironment;
|
||||
if (opts.baseUrl) env.CODEBUDY_BASE_URL = opts.baseUrl;
|
||||
|
||||
@@ -123,11 +175,17 @@ function buildChildEnv(opts) {
|
||||
* @param {string} o.cwd 工作目录(决定 CLI 能看到哪些项目文件)
|
||||
* @param {string} [o.homeDir] CLI 的配置/数据目录(默认 <os.tmpdir>/mcn-cli-home)
|
||||
* @param {string} [o.model] 模型 ID
|
||||
* @param {string} [o.apiKey] CODEBUDDY_API_KEY
|
||||
* @param {string} [o.apiKey] CODEBUDDY_API_KEY(优先级最高,一般留空走凭据文件)
|
||||
* @param {string} [o.authToken] CODEBUDDY_AUTH_TOKEN
|
||||
* @param {string} [o.credentialFile] 凭据文件路径,默认 ~/.workbuddy/mcn-workshop/ai-cli.json
|
||||
* @param {string} [o.internetEnvironment] internal / ioa / 留空
|
||||
* @param {string} [o.cliPath] 显式指定 CLI 路径
|
||||
* @param {number} [o.timeoutMs]
|
||||
* @param {boolean} [o.search] 是否允许联网搜索(默认 true)。true → 工具面收敛为 WebSearch+WebFetch
|
||||
* @param {string} [o.tools] 显式工具白名单:'default' | 'none' | 'WebSearch,WebFetch'(留空=按 search 推导)
|
||||
* @param {boolean} [o.strictMcp] 是否屏蔽宿主 MCP(默认 true)
|
||||
* @param {string} [o.appendSystemPrompt] 追加系统提示(如搜索使用策略)
|
||||
* @param {number} [o.maxTurns] 最大 agent 轮数(0=不限)
|
||||
* @param {function} o.onDelta (text) => void 文本增量
|
||||
* @param {function} [o.onThink] (text) => void 思考增量(可选)
|
||||
* @param {function} [o.onEvent] (obj) => void 全部原始事件(可选,调试用)
|
||||
@@ -154,6 +212,33 @@ function runCli(o) {
|
||||
...(o.permissionMode ? ['--permission-mode', o.permissionMode] : []),
|
||||
];
|
||||
|
||||
// ---- 工具面收敛(既提速,也是安全边界)----
|
||||
// 工作台的 AI 只负责「产出文本」,prompt 里已自带全部上下文(账号设定/需求/历史),
|
||||
// 不需要读写文件、跑命令。默认只给联网搜索两个工具:
|
||||
// · 启动快(不必加载 Bash/Edit/Write/... 的工具 schema)
|
||||
// · 模型物理上碰不到本机文件系统
|
||||
// o.search === false → 放开默认工具面,但明确禁用联网搜索
|
||||
let tools = o.tools;
|
||||
if (tools === undefined || tools === null || tools === '') {
|
||||
tools = (o.search === false) ? 'default' : 'WebSearch,WebFetch';
|
||||
}
|
||||
if (tools === 'none') args.push('--tools', '');
|
||||
else if (tools && tools !== 'default') args.push('--tools', tools);
|
||||
if (o.search === false) args.push('--disallowedTools', 'WebSearch,WebFetch');
|
||||
|
||||
// ---- 屏蔽宿主 MCP ----
|
||||
// 宿主会把 CODEBUDDY_MCP_CONFIG 注入进程树,里面挂着 weixinpay / sheetagent 等。
|
||||
// 工作台场景一个都用不上,加载它们纯属浪费启动时间,还可能被模型误调。
|
||||
if (o.strictMcp !== false) {
|
||||
args.push('--strict-mcp-config', '--mcp-config', '{"mcpServers":{}}');
|
||||
}
|
||||
|
||||
// 推理开销:把首字延迟从几十秒压到十几秒的关键开关(minimal|low|medium|high|xhigh|max)
|
||||
if (o.effort) args.push('--effort', o.effort);
|
||||
|
||||
if (o.appendSystemPrompt) args.push('--append-system-prompt', o.appendSystemPrompt);
|
||||
if (parseInt(o.maxTurns, 10) > 0) args.push('--max-turns', String(parseInt(o.maxTurns, 10)));
|
||||
|
||||
const env = buildChildEnv({
|
||||
homeDir,
|
||||
apiKey: o.apiKey,
|
||||
@@ -183,6 +268,7 @@ function runCli(o) {
|
||||
}
|
||||
|
||||
let full = '';
|
||||
let streamed = ''; // 已经通过 onDelta 推给前端的内容(用于终态比对)
|
||||
let stderr = '';
|
||||
let sessionId = '';
|
||||
let usage = null;
|
||||
@@ -190,6 +276,11 @@ function runCli(o) {
|
||||
let errText = '';
|
||||
let sawDelta = false;
|
||||
let buf = '';
|
||||
// 每次工具调用的发生位置(= 当时的 full 长度)。
|
||||
// 根因:模型在发起 WebSearch 前常先吐一句过程话术(如
|
||||
// "I'll search for ... before responding."),它不是答案。
|
||||
// 只保留「最后一次工具调用之后」的文本作为正文。
|
||||
const toolMarks = [];
|
||||
|
||||
const handleLine = (line) => {
|
||||
line = line.trim();
|
||||
@@ -206,6 +297,12 @@ function runCli(o) {
|
||||
// 文本增量:stream_event → content_block_delta → text_delta
|
||||
if (ev.type === 'stream_event' && ev.event) {
|
||||
const e2 = ev.event;
|
||||
// 工具调用开始的边界(用于切掉调用前的过程话术)
|
||||
if (e2.type === 'content_block_start' && e2.content_block &&
|
||||
(e2.content_block.type === 'tool_use' || e2.content_block.name)) {
|
||||
toolMarks.push(full.length);
|
||||
return;
|
||||
}
|
||||
if (e2.type === 'content_block_delta' && e2.delta) {
|
||||
if (e2.delta.type === 'text_delta' && e2.delta.text) {
|
||||
const prev = full;
|
||||
@@ -219,6 +316,7 @@ function runCli(o) {
|
||||
// 已误判过一次的话,本段的累积都作废
|
||||
if (isCliBoilerplate(prev)) { full = ''; errText = errText || prev; return; }
|
||||
sawDelta = true;
|
||||
streamed += e2.delta.text;
|
||||
if (o.onDelta) { try { o.onDelta(e2.delta.text); } catch (e) {} }
|
||||
} else if (e2.delta.type === 'thinking_delta' && e2.delta.thinking && o.onThink) {
|
||||
try { o.onThink(e2.delta.thinking); } catch (e) {}
|
||||
@@ -229,6 +327,10 @@ function runCli(o) {
|
||||
|
||||
// 兜底:未开启/未产生增量流时,整段文本在 assistant 消息里
|
||||
if (ev.type === 'assistant' && ev.message && Array.isArray(ev.message.content)) {
|
||||
// 工具调用边界(未走增量流时,tool_use 只会出现在 assistant 消息里)
|
||||
for (const c of ev.message.content) {
|
||||
if (c && c.type === 'tool_use') toolMarks.push(full.length);
|
||||
}
|
||||
if (!sawDelta) {
|
||||
const t = ev.message.content
|
||||
.filter((c) => c && c.type === 'text' && typeof c.text === 'string')
|
||||
@@ -236,6 +338,7 @@ function runCli(o) {
|
||||
if (t && !full) {
|
||||
if (isCliBoilerplate(t)) { errText = errText || t; return; }
|
||||
full = t;
|
||||
streamed += t;
|
||||
if (o.onDelta) { try { o.onDelta(t); } catch (e) {} }
|
||||
}
|
||||
}
|
||||
@@ -283,18 +386,22 @@ function runCli(o) {
|
||||
child.on('close', (code) => {
|
||||
clearTimeout(timer);
|
||||
if (buf.trim()) handleLine(buf);
|
||||
const text = full || resultText;
|
||||
if (aborted) return resolve({ ok: false, text, error: 'CLI 执行超时或被取消', sessionId, usage });
|
||||
// 切掉「最后一次工具调用之前」的过程话术:那部分不是答案。
|
||||
// (例:模型先输出 "I'll search for ... before responding." 再发起 WebSearch)
|
||||
const cut = toolMarks.length ? toolMarks[toolMarks.length - 1] : 0;
|
||||
const answer = (cut > 0 && cut < full.length) ? full.slice(cut).trim() : full;
|
||||
const text = answer || resultText;
|
||||
if (aborted) return resolve({ ok: false, text, streamed, error: 'CLI 执行超时或被取消', sessionId, usage });
|
||||
// 事件里明确报了错(如未认证)→ 优先按错误返回
|
||||
if (errText) {
|
||||
const raw = errText;
|
||||
return resolve({ ok: false, text, error: messageForCliError(raw) || raw, sessionId, usage, rawStderr: raw });
|
||||
return resolve({ ok: false, text, streamed, error: messageForCliError(raw) || raw, sessionId, usage, rawStderr: raw });
|
||||
}
|
||||
if (code !== 0 && !text) {
|
||||
const msg = (stderr || '').trim().split('\n').filter(Boolean).slice(-3).join(' | ');
|
||||
return resolve({ ok: false, text, error: messageForCliError(msg) || ('CLI 退出码 ' + code), sessionId, usage, rawStderr: msg });
|
||||
return resolve({ ok: false, text, streamed, error: messageForCliError(msg) || ('CLI 退出码 ' + code), sessionId, usage, rawStderr: msg });
|
||||
}
|
||||
resolve({ ok: true, text, sessionId, usage });
|
||||
resolve({ ok: true, text, streamed, sessionId, usage });
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -324,4 +431,4 @@ function messageForCliError(msg) {
|
||||
return msg;
|
||||
}
|
||||
|
||||
module.exports = { runCli, resolveCli, buildChildEnv };
|
||||
module.exports = { runCli, resolveCli, buildChildEnv, resolveCredentials, defaultCredentialFile };
|
||||
+11
-3
@@ -4,17 +4,25 @@
|
||||
"outputRoot": "MCNSkill项目",
|
||||
"businessDataDir": ["MCNSkill项目", "三方数据", "红狐数据", "抖音榜单"],
|
||||
"extraRoots": [],
|
||||
"sessionCwd": "D:\\AI技能\\mcn-workshop",
|
||||
|
||||
"ai": {
|
||||
"backend": "dify",
|
||||
"backend": "cli",
|
||||
"cli": {
|
||||
"path": "",
|
||||
"model": "custom:doubao-seed-2-1-pro-260628",
|
||||
"model": "deepseek-v4.1-flash",
|
||||
"effort": "low",
|
||||
"apiKey": "",
|
||||
"authToken": "",
|
||||
"credentialFile": "",
|
||||
"internetEnvironment": "internal",
|
||||
"permissionMode": "",
|
||||
"timeoutMs": 300000
|
||||
"timeoutMs": 300000,
|
||||
"search": true,
|
||||
"tools": "",
|
||||
"strictMcp": true,
|
||||
"appendSystemPrompt": "",
|
||||
"maxTurns": 6
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -6,7 +6,7 @@
|
||||
|
||||
## 触发链路(所有入口共用)
|
||||
|
||||
按钮点击 → 确认弹窗(confirmRun / confirmAiScript / confirmSaveAccount)→ `executeTask()` → `POST /api/run`(server.js 写 automations 表,scheduledAt=now+5s + next_run_at=now+5000,`skills` 数组→`skills_json` 参数级技能挂载)→ 客户端调度器拾取 → 左侧会话栏「mcn-work-shop」空间后台会话执行(cwd = 工作台目录,由 `server.js` 从 `ROOT` 动态推导)→ 轮询 `/api/run/status`(done/error/review)→ 完成刷新页面。
|
||||
按钮点击 → 确认弹窗(confirmRun / confirmAiScript / confirmSaveAccount)→ `executeTask()` → `POST /api/run`(server.js 写 automations 表,scheduledAt=now+5s + next_run_at=now+5000,`skills` 数组→`skills_json` 参数级技能挂载)→ 客户端调度器拾取 → 左侧会话栏「mcn-workshop」空间后台会话执行(cwd = 工作台目录,由 `server.js` 从 `ROOT` 动态推导)→ 轮询 `/api/run/status`(done/error/review)→ 完成刷新页面。
|
||||
|
||||
## 功能入口 × 技能调用契约总表
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
| 候选 | `==OPTS==` | 每行一个候选,格式「骨架词——具体展开」(≤45 字、无编号);结合本条创意/人设落地,禁干巴术语 | 解析为气泡下内嵌按钮行,点选=以整行文本发一轮 |
|
||||
| 需求 | `==REQ==` | 8 行清单:内容形式/一句话故事/开场钩子/故事爆点/故事爽点/故事框架/人物关系/其他要求(空项留空) | 解析回填需求框字段 |
|
||||
|
||||
**System Prompt 已抽为文件**:`mcn-work-shop/prompts/clarify.md`(==PROMPT== 与 ==CHANGELOG== 之间为正文,头尾说明不发模型)。**迭代 prompt 只改该文件**,server.js 启动按 mtime 缓存读取、占位符注入(`{{accountName}}/{{personaText}}/{{contentText}}/{{attachText}}/{{reqMdText}}/{{histText}}/{{turnText}}`),不再改 server.js 字符串。
|
||||
**System Prompt 已抽为文件**:`mcn-workshop/prompts/clarify.md`(==PROMPT== 与 ==CHANGELOG== 之间为正文,头尾说明不发模型)。**迭代 prompt 只改该文件**,server.js 启动按 mtime 缓存读取、占位符注入(`{{accountName}}/{{personaText}}/{{contentText}}/{{attachText}}/{{reqMdText}}/{{histText}}/{{turnText}}`),不再改 server.js 字符串。
|
||||
|
||||
**关键规则(clarify.md 内固化)**:
|
||||
- 候选骨架受控词表:内容形式(剧情/Vlog/口播/**视觉短片**)、故事框架(钩子前置/悬念反转/递进铺垫/先抑后扬)、开场钩子(悬念提问/反差画面/直击痛点/身份共鸣/利益承诺)、剧情人物关系(一次一层);每项必须结合本条创意给「具体形态」
|
||||
@@ -0,0 +1,283 @@
|
||||
# AI 链路:本地 CodeBuddy CLI 接入(A 方案)
|
||||
|
||||
> 2026-09-29 落地并实测通过。工作台的 AI 能力(需求打磨 / 创作)除 Dify 外,新增**本地 CodeBuddy CLI** 通路。
|
||||
> 实现:`cli-backend.js`(后端模块)+ `server.js` 的 `/api/ai/clarify`、`/api/ai/status`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 一句话
|
||||
|
||||
把「工作台 → 模型」从 **HTTP 调 Dify** 换成 **spawn 一个 `codebuddy` 子进程、用 stdio 通信**。
|
||||
**不涉及任何本地端口**,与 WorkBuddy 桌面程序完全解耦。
|
||||
|
||||
---
|
||||
|
||||
## 2. 为什么必须自备凭据(别再挖宿主那条路)
|
||||
|
||||
排查中钉死了三条硬结论:
|
||||
|
||||
| 结论 | 证据 |
|
||||
|:--|:--|
|
||||
| 宿主 gateway 端口/密码**每轮轮换** | 同机同天实测端口 `12134 → 13116 → 10261`;密码同步换 |
|
||||
| 唯一发现通道只存在于 WorkBuddy 派生进程树内 | `CODEBUDDY_SERVICE_PROXY_URL`;用户双击 `start.bat` 起的工作台拿不到 |
|
||||
| WorkBuddy **故意**不让子进程继承凭据 | 4 个环境变量交付 → CLI 读完**立即 `delete`**;凭据不落明文盘 |
|
||||
|
||||
→ 走官方文档给的非交互认证通道:`CODEBUDDY_API_KEY` / `CODEBUDDY_AUTH_TOKEN`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 配置步骤
|
||||
|
||||
### 步骤 1:拿 API Key
|
||||
|
||||
| 版本 | 地址 |
|
||||
|:--|:--|
|
||||
| **中国版**(本机是这版,`internetEnvironment=internal`) | https://copilot.tencent.com/profile/ |
|
||||
| 国际版 | https://www.codebuddy.ai/profile/keys |
|
||||
| iOA | https://tencent.sso.copilot.tencent.com/profile/keys |
|
||||
|
||||
### 步骤 2:把 Key 放进凭据文件(⛔ 不要写进 config.json)
|
||||
|
||||
**`config.json` 是 git 跟踪文件,写进去会提交到仓库。**
|
||||
|
||||
默认凭据文件(仓库之外,不会被技能同步脚本复制):
|
||||
|
||||
```
|
||||
%USERPROFILE%\.workbuddy\mcn-workshop\ai-cli.json
|
||||
```
|
||||
|
||||
内容:
|
||||
|
||||
```json
|
||||
{ "apiKey": "ck_xxxxxxxx._xxxxxxxx" }
|
||||
```
|
||||
|
||||
也可用 `authToken` 字段(优先级更高)。或直接设环境变量 `CODEBUDDY_API_KEY`。
|
||||
|
||||
### 步骤 3:自检
|
||||
|
||||
```bash
|
||||
curl "http://localhost:8900/api/ai/status" # 只报告配置,零消耗
|
||||
curl "http://localhost:8900/api/ai/status?probe=1" # 真跑一次极短提示
|
||||
```
|
||||
|
||||
期望 `"ready": true` 且 `probe.ok = true`。
|
||||
|
||||
---
|
||||
|
||||
## 4. 字段说明(`config.json` → `ai`)
|
||||
|
||||
| 字段 | 默认 | 说明 |
|
||||
|:--|:--|:--|
|
||||
| `backend` | `cli` | `"cli"` 走本地 CLI;`"dify"` 走原链路。环境变量 `MCN_AI_BACKEND` 可临时覆盖 |
|
||||
| `cli.path` | `''` | 留空自动探测:`CODEBUDDY_CLI_PATH` → WorkBuddy 自带 CLI → `PATH` |
|
||||
| `cli.model` | `deepseek-v4.1-flash` | **只影响 `/api/ai/clarify`**(需求打磨)。环境变量 `MCN_AI_MODEL` 可临时覆盖。⚠️ 创作任务的模型另见 §11 |
|
||||
| `cli.effort` | `low` | 推理档位 `minimal\|low\|medium\|high\|xhigh\|max`。**首字延迟第一大杠杆** |
|
||||
| `cli.search` | `true` | 是否允许联网搜索(见 §7) |
|
||||
| `cli.tools` | `''` | 工具白名单。留空=按 `search` 推导;`none`=全禁;也可写 `WebSearch,WebFetch` |
|
||||
| `cli.strictMcp` | `true` | 屏蔽宿主 MCP(weixinpay / sheetagent 等) |
|
||||
| `cli.maxTurns` | `6` | agent 轮数硬上限,防搜索失控跑飞 |
|
||||
| `cli.appendSystemPrompt` | `''` | 追加系统提示;留空时自动注入内置搜索策略(`CLI_SEARCH_HINT`) |
|
||||
| `cli.credentialFile` | `''` | 凭据文件路径;留空走默认位置 |
|
||||
| `cli.apiKey` / `cli.authToken` | `''` | 优先级最高,**但别填**(会被 git 记录) |
|
||||
| `cli.timeoutMs` | `300000` | 单次调用超时 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 三条必须保留的防护(改代码前务必读)
|
||||
|
||||
`cli-backend.js` 的 `buildChildEnv()` 做了三件事,每条都是实测踩出来的坑:
|
||||
|
||||
### 5.1 删掉 `SERVER__PORT` / `SERVER__HOST`
|
||||
|
||||
`codebuddy`(**连 `-p` 打印模式也**)内部会起一个 HTTP server:
|
||||
|
||||
```js
|
||||
listen(parseInt(process.env.SERVER__PORT) || config.get("cell.server", { port: 3000 }).port)
|
||||
```
|
||||
|
||||
在 WorkBuddy 进程树内启动 → 继承宿主的 `SERVER__PORT` → `EADDRINUSE` → **进程静默卡死**。
|
||||
迷惑性极强:HTTP 层(health / info / sessions)全正常,只有 agent 执行环节挂,
|
||||
极易被误判成「prewarm 占端口」。
|
||||
|
||||
官方自己也这么处理:`delete el.SERVER__PORT, delete el.SERVER__HOST`。
|
||||
|
||||
### 5.2 强制 `CODEBUDDY_FORCE_HEADLESS_BUNDLE=1`
|
||||
|
||||
`bin/codebuddy` launcher 按环境变量选 bundle:
|
||||
|
||||
| 环境变量 | 命中产物 |
|
||||
|:--|:--|
|
||||
| `CODEBUDDY_FORCE_LITE_WB_BUNDLE=1`(宿主注入,**优先级最高**) | `codebuddy-lite-wb.mjs`(WorkBuddy 特供精简包) |
|
||||
| `CODEBUDDY_FORCE_HEADLESS_BUNDLE=1` | `codebuddy-headless.js`(**我们要的**) |
|
||||
| 含 `--print` | 自动 headless |
|
||||
| 都没有 | `dist/codebuddy` → **本安装不存在**,报 `MODULE_NOT_FOUND` |
|
||||
|
||||
### 5.3 清掉宿主注入的会话上下文
|
||||
|
||||
`NODE_OPTIONS`(node-language-shim)、`CODEBUDDY_SESSION_*`、`CODEBUDDY_MCP_CONFIG`、
|
||||
`CODEBUDDY_GATEWAY_PASSWORD`、`BAGGAGE` 等一并删除,并把 `CODEBUDDY_CONFIG_DIR`
|
||||
指向独立目录(默认 `<tmpdir>/mcn-cli-home`),避免污染 WorkBuddy 自身配置。
|
||||
|
||||
---
|
||||
|
||||
## 6. 性能:三个真正的杠杆(实测数据)
|
||||
|
||||
| 杠杆 | 做法 | 效果 |
|
||||
|:--|:--|:--|
|
||||
| **① 工具面收敛** | `--tools "WebSearch,WebFetch"` | prompt 从 **21,776 → 4,859 tokens(-78%)**,耗时 **47.6s → 15.0s** |
|
||||
| **② 推理档位** | `--effort low` | 首字延迟 **61.7s → 19.7s** |
|
||||
| **③ 换模型** | 见 §9 | `deepseek-v4-pro` 46.2s vs `Claude-Opus-4.8` 18.5s(同一提示词) |
|
||||
|
||||
> ⚠️ `[INIT tools]` 事件里列的是**工具注册表全量**,不是实际启用集。
|
||||
> 判断 `--tools` 是否生效要看 `usage.input_tokens`,别被事件列表误导。
|
||||
|
||||
**额外收益**:工具面收敛后模型**物理上碰不到文件系统**(`Bash`/`Read`/`Write`/`Edit` 全不在白名单),
|
||||
这是免费拿到的一道安全边界。
|
||||
|
||||
---
|
||||
|
||||
## 7. 联网搜索
|
||||
|
||||
CLI **原生带** `WebSearch` / `WebFetch`,开箱可用,无需额外配置。实测:
|
||||
|
||||
```
|
||||
[TOOL_USE] WebSearch {"query":"今天热点新闻","freshness":"d1"}
|
||||
```
|
||||
|
||||
但搜索会把耗时推到 1–2 分钟,所以**不能简单地常开或常关**,而是给模型一条明确的判据。
|
||||
`server.js` 的 `CLI_SEARCH_HINT` 会作为 `--append-system-prompt` 注入:
|
||||
|
||||
- **该搜**:回答依赖「今天/近期」的外部事实(当下热点、平台规则与趋势、行业数据、事实核查)
|
||||
- **不该搜**:创作讨论、需求澄清、脚本撰写、文案打磨 → 一律不搜
|
||||
- **搜索预算**:最多 1 次 WebSearch(+ 必要 1 次 WebFetch),硬上限 2 次;搜到即答,禁止反复搜
|
||||
- 失败/超时直接凭已有信息作答,不重试
|
||||
|
||||
想完全关闭:`"search": false`(会自动改用 `--disallowedTools WebSearch,WebFetch`)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 输出协议
|
||||
|
||||
工作台对前端只暴露一套 SSE 协议,`cli` 与 `dify` 两条后端产出完全一致,**前端无需感知差异**:
|
||||
|
||||
| 事件 | 含义 |
|
||||
|:--|:--|
|
||||
| `{"delta": "..."}` | 文本增量,直接追加渲染 |
|
||||
| `{"replace": "..."}` | 整体覆盖(终态校准) |
|
||||
| `{"error": "..."}` | 错误(附可操作中文提示) |
|
||||
| `data: [DONE]` | 流结束 |
|
||||
|
||||
CLI 侧事件解析(`--output-format stream-json --include-partial-messages`):
|
||||
|
||||
| CLI 事件 | 处理 |
|
||||
|:--|:--|
|
||||
| `type:"system", subtype:"init"` | 取 `session_id` |
|
||||
| `stream_event` → `content_block_delta` → `delta.type==="text_delta"` | 取 `delta.text` 作为增量 |
|
||||
| `stream_event` → `content_block_start`(tool_use) | 记录**工具调用边界**,见下 |
|
||||
| `type:"assistant"` | 未产生增量流时的整段兜底 |
|
||||
| `type:"result"` | 终态。`is_error:true` 时真实原因在 **`errors[]`**,正文作废 |
|
||||
|
||||
### 两个必须处理的文本污染
|
||||
|
||||
1. **报错被当成回答**:未认证时 CLI 把报错文本走文本通道发出来 →
|
||||
`isCliBoilerplate()` 识别拦截,避免把英文报错显示成「AI 的回答」。
|
||||
2. **工具调用前的过程话术**:模型发起搜索前常先吐一句
|
||||
`I'll search for ... before responding.`,它不是答案。
|
||||
→ `runCli` 记录每次 `tool_use` 的位置 `toolMarks`,**只保留最后一次工具调用之后的文本**;
|
||||
由于这段已被流式推给前端,`server.js` 在成功分支比对 `r.text !== acc` 时补发 `{replace}` 整体覆盖。
|
||||
|
||||
---
|
||||
|
||||
## 9. 实测可用模型清单(本机 API Key 租户)
|
||||
|
||||
`--model` 的 `--help` 列表是**二进制里的静态清单,不等于已开通**。
|
||||
下表是逐个真跑出来的结果:
|
||||
|
||||
| 模型 ID | 真实名称 | 实测 |
|
||||
|:--|:--|:--|
|
||||
| `deepseek-v4.1-flash` | DeepSeek-V4.1-Flash | ✅ **当前默认**(`clarify`) |
|
||||
| `custom:custom-model-b5-standard` | GPT-5.5-Standard | ✅ 次选 |
|
||||
| `custom:custom-model-b5-priority` | GPT-5.5-Priority | ✅ 同族「优先档」(资源紧张时优先路由,通常积分倍率更高) |
|
||||
| `custom:custom-model-a4` | Claude-Opus-4.8 | ✅ |
|
||||
| `custom:custom-model-b1-standard` / `-priority` | GPT-5.4-Standard / -Priority | ✅ |
|
||||
| `custom:custom-model-b3-priority` | GPT-5.2-Priority | ✅ |
|
||||
| `custom:custom-model-b4-standard` / `-priority` | GPT-5.1-Standard / -Priority | ✅ |
|
||||
| `custom:custom-model-a1` | Claude-Opus-4.7 | ✅ |
|
||||
| `custom:custom-model-a2` | Claude-Sonnet-4.6 | ✅ |
|
||||
| `custom:custom-model-a3` | Claude-Opus-4.6 | ✅ |
|
||||
| `custom:custom-model-c2-flash` | Gemini-3.5-Flash-1 | ✅ |
|
||||
| `auto` / `hy3` / `glm-5.3` / `deepseek-v4-pro` | Auto / 混元3 / GLM-5.3 / DeepSeek-V4-Pro | ✅(deepseek 慢 3 倍) |
|
||||
| `custom:doubao-seed-2-1-pro-260628` | — | ❌ **500**,未开通 |
|
||||
| `custom:doubao-seed-2-0-lite-260428` | — | ❌ **400 `service info not found`** |
|
||||
|
||||
> **doubao 系在本机不可用**:既不在桌面产品配置的 48 个模型里,也不在该 API Key 租户的服务清单里。
|
||||
> CLI 的静态清单里出现它,只是「名单上有、没开通」。
|
||||
|
||||
### 速度对比 A:短提示词(约 130 字输出,含 ~10s CLI 启动开销)
|
||||
|
||||
| 模型 | 耗时 |
|
||||
|:--|:--|
|
||||
| GPT-5.5-Standard | 15.7s |
|
||||
| Claude-Opus-4.8 | 18.5s |
|
||||
| Kimi-K2.5 | 21.7s |
|
||||
| GLM-5.3 / GLM-5.3-Flash | 22.5s / 23.6s |
|
||||
| **DeepSeek-V4-Pro** | **46.2s** 🐢 |
|
||||
|
||||
### 速度对比 B:端到端走 `/api/ai/clarify` 真实需求打磨(含账号设定 + 完整模板)
|
||||
|
||||
| 模型 | 普通创作问答 | 问热点(触发搜索) | `==OPTS==/==REQ==` 合规 |
|
||||
|:--|:--|:--|:--|
|
||||
| **GPT-5.5-Standard**(当前) | **42.2s** | 137.2s | ✅ |
|
||||
| Claude-Opus-4.8 | 105.5s | 139.6s | ✅ |
|
||||
| DeepSeek-V4-Pro | 119.5s | 151.2s | ✅ |
|
||||
|
||||
> 结论:**普通问答差距最大(2.5 倍)**;一旦触发联网搜索,各模型都要 130–150s,
|
||||
> 瓶颈转移到搜索本身 → 这就是 `CLI_SEARCH_HINT` 要把搜索预算压到 ≤2 次的原因。
|
||||
|
||||
---
|
||||
|
||||
## 10. 排障速查
|
||||
|
||||
| 现象 | 原因 | 处理 |
|
||||
|:--|:--|:--|
|
||||
| 进程卡死无输出 | `SERVER__PORT` 未清除 → EADDRINUSE | 确认 §5.1 的 delete 生效 |
|
||||
| `Cannot find module '../dist/codebuddy'` | bundle 分流未命中 | 设 `CODEBUDDY_FORCE_HEADLESS_BUNDLE=1` |
|
||||
| `Authentication required. Please use /login` | 无凭据 | 按 §3 配 key |
|
||||
| `ready:false` 但 key 已填 | `internetEnvironment` 版本不对 | 中国版必须是 `internal` |
|
||||
| `500 internal server error` / `400 Custom model [...] service info not found` | 该模型未在当前 Key 租户开通 | 换 §9 表内 ✅ 的模型 |
|
||||
| 首字要等 60s+ | 推理档位过高 | `"effort": "low"` |
|
||||
| 回答里混进英文过程话术 | 工具调用前的叙述 | §8 的 `toolMarks` 切分(已内置) |
|
||||
| HTTP 层全正常、agent 却不干活 | 典型 EADDRINUSE 假象 | 看 §5.1 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 完整流程=两段,模型来源不同(⚠️ 容易搞混)
|
||||
|
||||
工作台「AI 创作」实际只有两个后端调用:
|
||||
|
||||
| 环节 | 接口 | 谁在跑 | 模型从哪来 |
|
||||
|:--|:--|:--|:--|
|
||||
| **需求打磨**(弹窗内对话) | `POST /api/ai/clarify` | **本方案**:本地 CLI 子进程 | `config.json → ai.cli.model` |
|
||||
| **提交创作任务**(生成脚本 S1-S11) | `POST /api/run` | **WorkBuddy 宿主**:写 automation,由桌面端调度器执行 | **`sessions` 表里最近活跃的「手动会话」的 `model`**,兜底 `auto` |
|
||||
|
||||
> 🔴 **两者互不相干。** 改 `ai.cli.model` **不会**影响创作任务用的模型。
|
||||
> 创作任务的模型是「跟随用户在 WorkBuddy UI 里最近选过的模型」——即浮动的,
|
||||
> 从工作台侧看**不确定**。查证方法:
|
||||
> ```sql
|
||||
> SELECT model_id, COUNT(*) FROM automations GROUP BY model_id ORDER BY 2 DESC;
|
||||
> ```
|
||||
> 本机实测历史分布:`hy4-preview` 30 条 / `deepseek-v4.1-flash` 6 条。
|
||||
> 想让创作任务固定用某个模型,需要在 UI 里把当前会话模型切到它(或改造 `/api/run` 支持显式 `model_id`)。
|
||||
|
||||
### 完整链路自检结果(2026-09-29,model = deepseek-v4.1-flash)
|
||||
|
||||
| 环节 | 结果 |
|
||||
|:--|:--|
|
||||
| `GET /api/ai/status` | backend=`cli` · model=`deepseek-v4.1-flash` · ready=**true** |
|
||||
| `GET /api/ai/status?probe=1` | ok=**true** · 17.0s · 返回「成功」 |
|
||||
| `POST /api/ai/clarify`(stream) | 371 字 · `==OPTS==/==REQ==` 合规 **✅** · 无英文残留 **✅** · 51.7s |
|
||||
| `POST /api/run` → 轮询状态 | queued(5s) → running(30s) → **done**(50s) · `model_id` 落库 = `deepseek-v4.1-flash` **✅** |
|
||||
|
||||
> 注:`/api/ai/topics` 是**遗留死代码** —— 前端从不调用它(选题列表走 `/api/dsh/topics`)。
|
||||
> 它仍写死 Dify,未配 `DIFY_MCN_CYLG_KEY` 时会 500,但**不影响真实流程**。
|
||||
File renamed without changes.
+1
-1
@@ -1,6 +1,6 @@
|
||||
# 短视频工作台 · 视觉与交互规范(最终态)
|
||||
|
||||
> **定位**:本文件是工作台前端(`mcn-work-shop/`)视觉与交互的**唯一权威**——设计 token、布局框架、**逐页最终态**、组件规格、交互链路、已知坑。
|
||||
> **定位**:本文件是工作台前端(`mcn-workshop/`)视觉与交互的**唯一权威**——设计 token、布局框架、**逐页最终态**、组件规格、交互链路、已知坑。
|
||||
> **用法**:新增页面 / 改样式 / 改交互前先读本文件;改完**回写本文件**(含文末变更记录),避免同类坑重复踩。
|
||||
> **技术基线**:零依赖 Node 服务 + 原生 JS(无框架无构建);`public/` 下 `index.html` / `style.css` / `app.js` / `data-pages.js` / `help-guide.js`。
|
||||
> **最后更新**:2026-09-16(本轮把此前的 `工作台UI规范.md` 合并升级为本文件并重命名为「视觉交互规范」,内容为**迭代后的最终态**)
|
||||
File renamed without changes.
File renamed without changes.
+18
-2
@@ -11,6 +11,10 @@ const DEFAULTS = {
|
||||
outputRoot: 'MCNSkill项目',
|
||||
businessDataDir: ['MCNSkill项目', '三方数据', '红狐数据', '抖音榜单'],
|
||||
extraRoots: [],
|
||||
// 10-08:工作台提交的 AI 任务会话归属哪个工作区 —— 侧边栏分组按会话 cwd 聚合,
|
||||
// cwd 不是已打开过的工作区就会落到「未分组任务」。
|
||||
// 留空 → 用工作台自身目录(ROOT);填绝对路径 → 会话归到那个工作区(改分组只改这里,不动代码)。
|
||||
sessionCwd: '',
|
||||
// 09-29 AI 通路配置:backend 决定走哪条链路
|
||||
// "cli" 本地 CodeBuddy CLI 进程(stdio,无端口;需自备 CODEBUDDY_API_KEY)
|
||||
// "dify" Dify chat-messages(原链路,保留兜底)
|
||||
@@ -19,11 +23,23 @@ const DEFAULTS = {
|
||||
cli: {
|
||||
path: '', // 留空 → 自动探测(环境变量 CODEBUDDY_CLI_PATH > WorkBuddy 自带 CLI > PATH)
|
||||
model: 'custom:doubao-seed-2-1-pro-260628',
|
||||
apiKey: '', // 留空 → 读环境变量 CODEBUDDY_API_KEY
|
||||
authToken: '', // 留空 → 读环境变量 CODEBUDDY_AUTH_TOKEN(优先级最高)
|
||||
apiKey: '', // 一般留空:config.json 是 git 跟踪文件,key 别写这里
|
||||
authToken: '', // 同上
|
||||
// 凭据文件(JSON:{"apiKey":"..."} 或 {"authToken":"..."})。
|
||||
// 留空 → 默认 ~/.workbuddy/mcn-workshop/ai-cli.json(刻意放仓库外,不会被技能同步复制)
|
||||
credentialFile: '',
|
||||
internetEnvironment: 'internal', // 中国版=internal,iOA=ioa,国际版留空
|
||||
permissionMode: '',
|
||||
timeoutMs: 300000,
|
||||
// 推理开销档位:minimal|low|medium|high|xhigh|max(留空=模型默认)。
|
||||
// 这是首字延迟的第一大杠杆:reasoning 模型默认档位会让首字等到 60s+。
|
||||
effort: 'low',
|
||||
// 联网搜索:true → 工具面收敛为 WebSearch+WebFetch(启动快,且模型碰不到文件系统)
|
||||
search: true,
|
||||
tools: '', // 显式覆盖工具白名单:'' = 按 search 推导;'none' = 全禁
|
||||
strictMcp: true, // 屏蔽宿主 MCP(weixinpay/sheetagent 等)
|
||||
appendSystemPrompt: '', // 追加系统提示
|
||||
maxTurns: 6, // agent 轮数硬上限(防搜索/工具调用失控跑飞)
|
||||
},
|
||||
},
|
||||
};
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+58
-2
@@ -70,7 +70,8 @@ async function executeTask(opts) {
|
||||
});
|
||||
if (d.error) throw new Error(d.error);
|
||||
taskId = d.id;
|
||||
showToast('✅ 任务已提交,请在左侧会话栏查看执行');
|
||||
showToast('✅ 任务已提交,执行进度见右下角面板');
|
||||
TaskProgress.show(opts.label || '工作台任务');
|
||||
if (opts.onSubmit) { try { opts.onSubmit(taskId); } catch (e) {} }
|
||||
} catch (e) {
|
||||
showToast('❌ 任务提交失败: ' + e.message);
|
||||
@@ -82,6 +83,7 @@ async function executeTask(opts) {
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise((r) => setTimeout(r, 5000));
|
||||
try { s = await api('/api/run/status?id=' + encodeURIComponent(taskId)); } catch (e) { continue; }
|
||||
if (s) TaskProgress.update(s, opts.label || '工作台任务');
|
||||
if (!s || s.state === 'notfound') { finished = true; finish(s || null); break; }
|
||||
if (s.state === 'done') { finished = true; finish(s); showToast('✅ 任务执行完成,页面已自动刷新'); refreshCurrentPage(); break; }
|
||||
if (s.state === 'error') { finished = true; finish(s); showToast('❌ 任务执行失败' + (s.error ? ':' + s.error : '')); break; }
|
||||
@@ -90,7 +92,9 @@ async function executeTask(opts) {
|
||||
}
|
||||
restore();
|
||||
if (!ended) finish(null);
|
||||
if (!finished) showToast('⏳ 任务仍在执行,请到左侧会话栏查看进度');
|
||||
if (!finished) showToast('⏳ 任务仍在执行,右下角面板可继续观察进度');
|
||||
// 终态保留 8 秒展示结果摘要(状态/工具调用/最近动作)后自动收起;未完成则保留面板供继续观察
|
||||
if (finished) setTimeout(() => TaskProgress.hide(), 8000);
|
||||
}
|
||||
|
||||
// 通用确认弹窗:确认后执行 AI 任务(opts: title/desc/target/url/prompt/label/btn/afterText)
|
||||
@@ -263,6 +267,58 @@ function showToast(msg) {
|
||||
_toastTimer = setTimeout(() => t.classList.remove('show'), 1800);
|
||||
}
|
||||
|
||||
/* ================= 任务进度面板(10-08 新增) =================
|
||||
后台任务原本只弹一句「已提交」,用户无法判断到底在跑还是卡住、做了什么。
|
||||
本面板把 /api/run/status 的进度(状态/耗时/工具调用/最近动作/是否停滞)持续显示,
|
||||
不必再去左侧会话栏翻找。用法:TaskProgress.show(标题) → 轮询中 TaskProgress.update(s) → TaskProgress.hide() */
|
||||
const TaskProgress = (function () {
|
||||
let el = null;
|
||||
const fmtDur = (ms) => {
|
||||
if (ms == null) return '—';
|
||||
const s = Math.max(0, Math.round(Number(ms) / 1000));
|
||||
if (s < 60) return s + ' 秒';
|
||||
return Math.floor(s / 60) + ' 分 ' + (s % 60) + ' 秒';
|
||||
};
|
||||
const STATE_TXT = { queued: '排队中', pending: '等待调度', running: '执行中', done: '已完成', error: '执行失败', review: '待确认', notfound: '任务已清理' };
|
||||
function ensure() {
|
||||
if (el && document.body.contains(el)) return el;
|
||||
el = document.createElement('div');
|
||||
el.className = 'tp-panel';
|
||||
document.body.appendChild(el);
|
||||
return el;
|
||||
}
|
||||
function render(title, s) {
|
||||
const box = ensure();
|
||||
const st = (s && s.state) || 'queued';
|
||||
const dotCls = st === 'running' ? 'run' : (st === 'done' ? 'done' : (st === 'error' ? 'err' : ''));
|
||||
const tc = (s && s.toolCalls) ? s.toolCalls : {};
|
||||
const tools = Object.keys(tc).map((k) => '<span>' + esc(k) + ' ×' + tc[k] + '</span>').join('');
|
||||
const stalled = st === 'running' && !!(s && s.stalled);
|
||||
const ago = (s && s.updatedAgoMs != null) ? fmtDur(s.updatedAgoMs) : null;
|
||||
box.innerHTML =
|
||||
'<div class="tp-head"><i class="tp-dot ' + dotCls + '"></i>'
|
||||
+ '<div class="tp-title">' + esc(title || (s && s.sessionTitle) || 'AI 任务') + '</div>'
|
||||
+ '<button class="tp-close" title="关闭">×</button></div>'
|
||||
+ '<div class="tp-body">'
|
||||
+ '<div class="tp-row"><span class="k">状态</span><span class="v">' + esc(STATE_TXT[st] || st)
|
||||
+ (s && s.elapsedMs != null ? '(已运行 ' + fmtDur(s.elapsedMs) + ')' : '') + '</span></div>'
|
||||
+ (tools ? '<div class="tp-row"><span class="k">工具</span><span class="v"><div class="tp-tools">' + tools + '</div></span></div>' : '')
|
||||
+ (s && s.lastAction ? '<div class="tp-row"><span class="k">最近动作</span><span class="v"></span></div><div class="tp-act">' + esc(s.lastAction) + '</div>' : '')
|
||||
+ (ago && st === 'running' ? '<div class="tp-row" style="margin-top:6px"><span class="k">活跃</span><span class="v">'
|
||||
+ (stalled ? '已 ' : '') + esc(ago) + '前有输出</span></div>' : '')
|
||||
+ (stalled ? '<div class="tp-warn">⚠️ 超过 90 秒没有新输出,可能卡住或等待中</div>' : '')
|
||||
+ (s && s.error ? '<div class="tp-warn">错误:' + esc(String(s.error)) + '</div>' : '')
|
||||
+ '</div>';
|
||||
const btn = box.querySelector('.tp-close');
|
||||
if (btn) btn.onclick = hide;
|
||||
}
|
||||
function show(title, s) { render(title, s || null); }
|
||||
function update(s, title) { render(title, s); }
|
||||
function hide() { if (el && el.parentNode) el.parentNode.removeChild(el); el = null; }
|
||||
return { show, update, hide };
|
||||
})();
|
||||
window.TaskProgress = TaskProgress;
|
||||
|
||||
/* ================= 迷你 Markdown 渲染器 ================= */
|
||||
function inlineMd(s) {
|
||||
let out = esc(s);
|
||||
+22
-2
@@ -1056,19 +1056,32 @@ async function updateRankingData(view, btn) {
|
||||
body: JSON.stringify({ prompt: rankPrompt, force: !!inCooldown }),
|
||||
});
|
||||
if (!r || r.ok === false) throw new Error((r && r.error) || '任务提交失败');
|
||||
showToast('✅ 榜单更新任务已提交,请到左侧会话栏查看执行');
|
||||
showToast('✅ 榜单更新任务已提交,执行进度见右下角面板');
|
||||
TaskProgress.show('更新榜单数据');
|
||||
// 4) 轮询任务状态(最长 15 分钟;期间按钮保持 busy)
|
||||
// 10-08 修复:工作台服务被回收时 api() 会持续抛错,原逻辑静默 continue 直到 15 分钟超时、用户零感知
|
||||
// ⇒ 连续 3 次查询失败即判定服务已断开,提示并结束轮询(任务仍在后台跑,只是工作台看不到进度)
|
||||
const deadline = Date.now() + 15 * 60 * 1000;
|
||||
let state = 'running';
|
||||
let pollFail = 0;
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise((res) => setTimeout(res, 5000));
|
||||
let s = null;
|
||||
try { s = await api('/api/run/status?id=' + encodeURIComponent(r.id)); } catch (e) { continue; }
|
||||
try { s = await api('/api/run/status?id=' + encodeURIComponent(r.id)); pollFail = 0; if (s) TaskProgress.update(s, '更新榜单数据'); }
|
||||
catch (e) {
|
||||
if (++pollFail >= 3) {
|
||||
restore();
|
||||
showToast('⚠️ 工作台服务已断开(连续 3 次查询失败);任务仍在后台执行,重启工作台后请到左侧会话栏查看进度');
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!s || s.state === 'notfound') { state = 'done'; break; }
|
||||
if (s.state === 'done') { state = 'done'; break; }
|
||||
if (s.state === 'error' || s.state === 'review') { state = s.state; break; }
|
||||
}
|
||||
restore();
|
||||
if (state !== 'running') setTimeout(() => TaskProgress.hide(), 8000);
|
||||
if (state === 'done') {
|
||||
showToast('✅ 榜单数据已更新,正在刷新…');
|
||||
// 5) 整页重建:丢弃 _reload(同闭包 render 不清 meta 缓存)→ router 重新进入渲染 → 拉到最新数据期
|
||||
@@ -1216,12 +1229,15 @@ function updateFollowedAccounts(btn) {
|
||||
});
|
||||
if (r.error) throw new Error(r.error);
|
||||
const taskId = r.id;
|
||||
const taskTitle = '更新账号·' + name;
|
||||
TaskProgress.show(taskTitle);
|
||||
const deadline = Date.now() + 20 * 60 * 1000;
|
||||
let state = 'running';
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise((res) => setTimeout(res, 5000));
|
||||
let s = null;
|
||||
try { s = await api('/api/run/status?id=' + encodeURIComponent(taskId)); } catch (e) { continue; }
|
||||
if (s) TaskProgress.update(s, taskTitle);
|
||||
if (!s || s.state === 'notfound') { state = 'done'; break; }
|
||||
if (s.state === 'done') { state = 'done'; break; }
|
||||
if (s.state === 'error' || s.state === 'review') { state = s.state; break; }
|
||||
@@ -1231,6 +1247,7 @@ function updateFollowedAccounts(btn) {
|
||||
} catch (e) { failed.push(name + '(' + e.message + ')'); }
|
||||
}
|
||||
restore();
|
||||
TaskProgress.hide();
|
||||
if (btn) btn.blur();
|
||||
showToast(done.length ? `✅ 已更新 ${done.length} 个关注账号${failed.length ? `;${failed.length} 个未完成:${failed.join('、')}` : ''}` : '❌ 更新未完成:' + failed.join('、'));
|
||||
refreshCurrentPage();
|
||||
@@ -1950,12 +1967,15 @@ function readVideoTaskRec(videoId) {
|
||||
/* 刷新后恢复轮询:跟随该视频解析任务直到终态 → 清理记录 → 提示并刷新本页按钮态 */
|
||||
async function watchVideoParseTask(videoId, taskId, reload) {
|
||||
let s = null;
|
||||
TaskProgress.show('视频解析');
|
||||
const deadline = Date.now() + 35 * 60 * 1000;
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise((r) => setTimeout(r, 5000));
|
||||
try { s = await api('/api/run/status?id=' + encodeURIComponent(taskId)); } catch (e) { continue; }
|
||||
if (s) TaskProgress.update(s, '视频解析');
|
||||
if (!s || s.state === 'notfound' || s.state === 'done' || s.state === 'error' || s.state === 'review') break;
|
||||
}
|
||||
setTimeout(() => TaskProgress.hide(), 8000);
|
||||
try { localStorage.removeItem(VIDEO_TASK_KEY(videoId)); } catch (e) {}
|
||||
if (s && s.state === 'done') showToast('✅ 视频解析完成,已自动刷新本页');
|
||||
else if (s && s.state === 'error') showToast('❌ 视频解析失败' + (s.error ? ':' + s.error : '') + '(任务已结束,可重新点击解析)');
|
||||
File renamed without changes.
File renamed without changes.
+25
@@ -880,3 +880,28 @@ td.td-title { max-width: 240px; }
|
||||
.g-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
|
||||
.g-actions .btn-sm { padding: 4px 12px; font-size: 14px; }
|
||||
.g-actions .ghost:disabled { opacity: .5; cursor: default; }
|
||||
|
||||
/* ===== 任务进度面板(10-08 新增:后台 AI 任务执行过程可见) ===== */
|
||||
.tp-panel {
|
||||
position: fixed; right: 18px; bottom: 18px; z-index: 9999;
|
||||
width: 380px; max-width: calc(100vw - 36px);
|
||||
background: var(--panel); border: 1px solid var(--border); border-radius: 12px;
|
||||
box-shadow: 0 8px 28px rgba(0,0,0,.16); overflow: hidden;
|
||||
font-size: 13.5px; color: var(--text);
|
||||
}
|
||||
.tp-head { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--border); background: var(--primary-soft); }
|
||||
.tp-title { font-weight: 700; font-size: 14px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.tp-close { border: none; background: transparent; color: var(--dim); cursor: pointer; font-size: 16px; line-height: 1; padding: 2px 4px; }
|
||||
.tp-body { padding: 10px 12px; }
|
||||
.tp-row { display: flex; gap: 8px; margin-bottom: 6px; }
|
||||
.tp-row .k { color: var(--dim); flex: 0 0 62px; }
|
||||
.tp-row .v { flex: 1; word-break: break-all; }
|
||||
.tp-tools { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.tp-tools span { background: #f0f2f5; border-radius: 20px; padding: 2px 9px; font-size: 12px; font-variant-numeric: tabular-nums; }
|
||||
.tp-act { max-height: 92px; overflow: auto; background: #fafbfc; border: 1px solid var(--border); border-radius: 8px; padding: 8px 9px; color: var(--dim); font-size: 12.5px; line-height: 1.55; white-space: pre-wrap; word-break: break-word; }
|
||||
.tp-warn { margin-top: 8px; padding: 7px 9px; border-radius: 8px; background: #fff3e6; color: #b3541e; font-size: 12.5px; font-weight: 600; }
|
||||
.tp-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--primary); flex: 0 0 8px; }
|
||||
.tp-dot.run { animation: tp-pulse 1.1s ease-in-out infinite; }
|
||||
.tp-dot.done { background: #2f9e44; }
|
||||
.tp-dot.err { background: var(--danger); }
|
||||
@keyframes tp-pulse { 0%,100% { opacity: 1; } 50% { opacity: .25; } }
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+128
-40
@@ -13,7 +13,7 @@ const dsh = require('./dsh-data');
|
||||
const { parseAttachment } = require('./scripts/attachment-parser');
|
||||
const { loadConfig } = require('./load-config');
|
||||
// 09-29 新增:本地 CodeBuddy CLI 后端(stdio 直连,替代 Dify;见 cli-backend.js 头部说明)
|
||||
const { runCli, resolveCli } = require('./cli-backend');
|
||||
const { runCli, resolveCli, resolveCredentials } = require('./cli-backend');
|
||||
|
||||
// 09-01 默认端口由 8899 改为 8900:本机 8899 被小米 PC 管家 MiPCAudio.exe 系统服务占用(0.0.0.0 监听且自动复活),
|
||||
// 此时 127.0.0.1:8899 绑定不生效 → 默认直接起在 8900;仍可 `node server.js <端口>` 显式换端口
|
||||
@@ -34,10 +34,46 @@ function aiBackend() {
|
||||
return String((CFG.ai && CFG.ai.backend) || 'dify').toLowerCase();
|
||||
}
|
||||
|
||||
// 09-29 CLI 后端的联网搜索策略(追加到系统提示)。
|
||||
// 根因:CLI 自带 WebSearch/WebFetch,但默认不会主动用;而每次都搜又会把首字延迟从十几秒拖到几十秒。
|
||||
// 所以给模型一条明确的「什么时候才搜」的判据,而不是简单开关。
|
||||
const CLI_SEARCH_HINT =
|
||||
'你具备联网搜索能力(WebSearch / WebFetch),但搜索很慢,要严格按判据使用。'
|
||||
+ '【该搜】回答依赖「今天/近期」的外部事实时:当下热点、平台规则与趋势、行业数据、事实核查。'
|
||||
+ '【不该搜】创作讨论、需求澄清、脚本撰写、文案打磨——这些靠你的知识直接答,一律不搜。'
|
||||
+ '【搜索预算】中文关键词;最多 1 次 WebSearch;仅当搜索结果指向必须打开的页面时才追加 1 次 WebFetch;'
|
||||
+ '工具调用总数硬上限 2 次。搜到即答,禁止反复搜或换词重搜。'
|
||||
+ '搜索失败或超时就直接凭已有信息作答,不要重试。';
|
||||
|
||||
// 由 config.json 的 ai.cli 段构造一次 CLI 调用参数(clarify / 自检 / 后续其它入口共用,避免多处硬编码)
|
||||
function cliBaseOpts(over) {
|
||||
const c = (CFG.ai && CFG.ai.cli) || {};
|
||||
const searchOn = c.search !== false;
|
||||
return Object.assign({
|
||||
cwd: ROOT,
|
||||
// MCN_AI_MODEL 可临时覆盖模型(用于不重启、不改配置的 A/B 对比与排障)
|
||||
model: String(process.env.MCN_AI_MODEL || '').trim() || c.model,
|
||||
apiKey: c.apiKey,
|
||||
authToken: c.authToken,
|
||||
credentialFile: c.credentialFile,
|
||||
internetEnvironment: c.internetEnvironment,
|
||||
permissionMode: c.permissionMode,
|
||||
cliPath: c.path,
|
||||
timeoutMs: Math.max(30000, parseInt(c.timeoutMs, 10) || 300000),
|
||||
effort: c.effort || '',
|
||||
search: searchOn,
|
||||
tools: c.tools,
|
||||
strictMcp: c.strictMcp !== false,
|
||||
maxTurns: c.maxTurns,
|
||||
appendSystemPrompt: c.appendSystemPrompt || (searchOn ? CLI_SEARCH_HINT : ''),
|
||||
}, over || {});
|
||||
}
|
||||
|
||||
// 09-10 路径治本(配套「仓库特征判定」):工作台运行时路径一律从 ROOT 动态推导,禁止写死盘符。
|
||||
// 根因:原写死盘符指向仓库外的固定目录,仓库迁移后该目录不存在 → 浏览器锁失效、会话 cwd 悬空。
|
||||
const BROWSER_LOCK_FILE = path.join(ROOT, '.browser-lock'); // 浏览器互斥锁文件(与工作台同目录)
|
||||
const SESSION_CWD = ROOT; // 工作台触发的 AI 会话 cwd(分组名 = mcn-work-shop)
|
||||
// 10-08:默认工作台自身目录;config.json 的 sessionCwd 可指定别的工作区(分组随之改变)
|
||||
const SESSION_CWD = String(CFG.sessionCwd || '').trim() || ROOT;
|
||||
|
||||
// 09-07:prompt 模板文件化(prompts/*.md)——规则迭代只改文件不碰代码;文件缺失/损坏回退 null,调用方自行兜底。
|
||||
// 模板正文取 ==PROMPT== 与 ==CHANGELOG== 之间(头部说明/文末变更记录不发给模型)
|
||||
@@ -359,7 +395,7 @@ function createOnceAutomation(name, prompt, skillsArr, connectorIdsArr) {
|
||||
try {
|
||||
const now = Date.now();
|
||||
const id = 'automation-' + now;
|
||||
const cwd = SESSION_CWD; // 工作台触发的会话归入 mcn-work-shop 空间分组(由 ROOT 动态推导)
|
||||
const cwd = SESSION_CWD; // 工作台触发的会话归入 mcn-workshop 空间分组(由 ROOT 动态推导)
|
||||
const d = new Date(now + 5 * 1000);
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
const scheduledAt = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||
@@ -401,6 +437,55 @@ function rankLastTaskState() {
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
// ---------- 任务进度可见(10-08 新增:用户「我需要看到 才知道执行的是否正常」)----------
|
||||
// 后台任务只返回 running/done 太粗:用户无法判断"在真跑"还是"卡住了"。
|
||||
// 解法:automation → 关联它拉起的后台会话 → 读该会话 jsonl 尾部 → 产出
|
||||
// 已运行时长 / 工具调用统计 / 最近一段动作 / 会话最后写入距今(判断是否停滞)
|
||||
|
||||
// 会话 cwd → WorkBuddy projects 目录名(实测:`D:\a\b\V1.0\mcn-workshop` → `d-a-b-V1.0-mcn-workshop`)
|
||||
function escapeCwdForProjects(cwd) {
|
||||
const s = String(cwd || '').replace(/\\/g, '/');
|
||||
const m = s.match(/^([A-Za-z]):\/(.+)$/);
|
||||
if (!m) return null;
|
||||
return m[1].toLowerCase() + '-' + m[2].replace(/\//g, '-');
|
||||
}
|
||||
|
||||
const TOOL_NAME_RE = /"name":"(Bash|Read|Write|Edit|Grep|Glob|WebSearch|WebFetch|WebFetch2|Skill|Task|Agent|TodoWrite|ExitPlanMode|mcp__[A-Za-z0-9_]+)"/g;
|
||||
|
||||
function readSessionProgress(sessionId, cwd) {
|
||||
const out = { lastAction: '', toolCalls: {}, updatedAgoMs: null, stalled: false, logBytes: 0 };
|
||||
try {
|
||||
const esc = escapeCwdForProjects(cwd);
|
||||
if (!esc) return out;
|
||||
const f = path.join(os.homedir(), '.workbuddy', 'projects', esc, sessionId + '.jsonl');
|
||||
if (!fs.existsSync(f)) return out;
|
||||
const st = fs.statSync(f);
|
||||
out.logBytes = st.size;
|
||||
out.updatedAgoMs = Date.now() - Number(st.mtimeMs);
|
||||
// 90 秒没再写入即视为停滞(模型正常推进时几乎每秒都在写日志)
|
||||
out.stalled = out.updatedAgoMs > 90000;
|
||||
if (st.size <= 0) return out;
|
||||
const readLen = Math.min(st.size, 256 * 1024);
|
||||
const fd = fs.openSync(f, 'r');
|
||||
const buf = Buffer.alloc(readLen);
|
||||
fs.readSync(fd, buf, 0, readLen, st.size - readLen);
|
||||
fs.closeSync(fd);
|
||||
const txt = buf.toString('utf8');
|
||||
let m;
|
||||
TOOL_NAME_RE.lastIndex = 0;
|
||||
while ((m = TOOL_NAME_RE.exec(txt))) out.toolCalls[m[1]] = (out.toolCalls[m[1]] || 0) + 1;
|
||||
const texts = [];
|
||||
const re2 = /"text":"((?:[^"\\]|\\.){4,})"/g;
|
||||
while ((m = re2.exec(txt))) {
|
||||
let s = m[1];
|
||||
try { s = JSON.parse('"' + s + '"'); } catch (e) { /* 解析失败保留原串 */ }
|
||||
if (s && String(s).trim()) texts.push(String(s).trim());
|
||||
}
|
||||
if (texts.length) out.lastAction = texts[texts.length - 1].replace(/\s+/g, ' ').slice(0, 160);
|
||||
} catch (e) { /* 读不到不影响状态判定 */ }
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- 静态文件 / API ----
|
||||
const server = http.createServer((req, res) => {
|
||||
const u = new URL(req.url, 'http://localhost');
|
||||
@@ -777,7 +862,7 @@ const server = http.createServer((req, res) => {
|
||||
const db = new DatabaseSync(process.env.WORKBUDDY_DB || path.join(os.homedir(), '.workbuddy', 'workbuddy.db'));
|
||||
const now = Date.now();
|
||||
const id = 'automation-' + now;
|
||||
const cwd = SESSION_CWD; // 工作台触发的会话归入 mcn-work-shop 空间分组(由 ROOT 动态推导)
|
||||
const cwd = SESSION_CWD; // 工作台触发的会话归入 mcn-workshop 空间分组(由 ROOT 动态推导)
|
||||
const d = new Date(Date.now() + 5 * 1000); // 未来 5 秒(08-31 根因:客户端只对未来 scheduledAt 补算 next_run_at;写 now=过去时间→不补算→调度器扫不到→卡死)
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
const scheduledAt = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||
@@ -809,10 +894,17 @@ const server = http.createServer((req, res) => {
|
||||
try {
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const db = new DatabaseSync(process.env.WORKBUDDY_DB || path.join(os.homedir(), '.workbuddy', 'workbuddy.db'), { readOnly: true });
|
||||
const auto = db.prepare('SELECT status, deleted_at, last_run_at FROM automations WHERE id=?').get(id);
|
||||
const auto = db.prepare('SELECT status, deleted_at, last_run_at, created_at FROM automations WHERE id=?').get(id);
|
||||
if (!auto || auto.deleted_at) { db.close(); return sendJSON(200, { ok: true, id, state: 'notfound' }); }
|
||||
const st = db.prepare('SELECT running, last_error FROM automation_runtime_state WHERE automation_id=?').get(id);
|
||||
const runs = db.prepare("SELECT status FROM automation_runs WHERE automation_id=? ORDER BY created_at DESC LIMIT 1").get(id);
|
||||
// 10-08:关联本任务拉起的后台会话(任务创建后 5 分钟内第一条自动化会话),读其日志产出进度
|
||||
const t0 = Number(auto.created_at) || 0;
|
||||
const sess = t0 ? db.prepare(
|
||||
'SELECT id, title, cwd FROM sessions WHERE is_background_automation=1 AND created_at >= ? AND created_at <= ? ORDER BY created_at ASC LIMIT 1'
|
||||
).get(t0 - 15000, t0 + 300000) : null;
|
||||
const prog = sess ? readSessionProgress(sess.id, sess.cwd) : null;
|
||||
const elapsedMs = t0 ? Date.now() - t0 : null;
|
||||
db.close();
|
||||
let state = 'queued';
|
||||
if (st && Number(st.running) === 1) state = 'running';
|
||||
@@ -824,7 +916,17 @@ const server = http.createServer((req, res) => {
|
||||
else if (['ERROR', 'CANCELLED', 'FAILED', 'INTERRUPTED'].includes(s)) state = 'error';
|
||||
else state = 'pending'; // ACCEPTED / QUEUED / PENDING 等 = 排队等待执行
|
||||
}
|
||||
return sendJSON(200, { ok: true, id, state, error: (st && st.last_error) || null });
|
||||
return sendJSON(200, {
|
||||
ok: true, id, state, error: (st && st.last_error) || null,
|
||||
elapsedMs,
|
||||
sessionId: (sess && sess.id) || null,
|
||||
sessionTitle: (sess && sess.title) || null,
|
||||
lastAction: (prog && prog.lastAction) || '',
|
||||
toolCalls: (prog && prog.toolCalls) || {},
|
||||
updatedAgoMs: (prog && prog.updatedAgoMs) != null ? prog.updatedAgoMs : null,
|
||||
stalled: !!(prog && prog.stalled),
|
||||
logBytes: (prog && prog.logBytes) || 0,
|
||||
});
|
||||
} catch (e) {
|
||||
return sendErr(500, '状态查询失败: ' + e.message);
|
||||
}
|
||||
@@ -888,7 +990,7 @@ const server = http.createServer((req, res) => {
|
||||
resp = await fetch(difyUrl + '/chat-messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ inputs: { is_think: '0', is_online: '0', model: process.env.DIFY_MCN_MODEL || 'gemini' }, query, response_mode: 'blocking', user: 'mcn-work-shop' }),
|
||||
body: JSON.stringify({ inputs: { is_think: '0', is_online: '0', model: process.env.DIFY_MCN_MODEL || 'gemini' }, query, response_mode: 'blocking', user: 'mcn-workshop' }),
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
} finally { clearTimeout(timer); }
|
||||
@@ -915,10 +1017,7 @@ const server = http.createServer((req, res) => {
|
||||
if (!t) return '';
|
||||
return t.length <= 8 ? '***' : t.slice(0, 6) + '***' + t.slice(-4);
|
||||
};
|
||||
const envKey = process.env.CODEBUDDY_API_KEY || '';
|
||||
const envTok = process.env.CODEBUDDY_AUTH_TOKEN || '';
|
||||
const effKey = c.apiKey || envKey;
|
||||
const effTok = c.authToken || envTok;
|
||||
const cred = resolveCredentials({ apiKey: c.apiKey, authToken: c.authToken, credentialFile: c.credentialFile });
|
||||
const info = {
|
||||
backend,
|
||||
cli: {
|
||||
@@ -926,10 +1025,11 @@ const server = http.createServer((req, res) => {
|
||||
pathConfigured: !!c.path,
|
||||
model: c.model || '',
|
||||
internetEnvironment: c.internetEnvironment || '',
|
||||
credential: effTok ? 'CODEBUDDY_AUTH_TOKEN(' + mask(effTok) + ')'
|
||||
: effKey ? 'CODEBUDDY_API_KEY(' + mask(effKey) + ')'
|
||||
: '(未配置)',
|
||||
ready: !!(effTok || effKey),
|
||||
credential: cred.source + (cred.apiKey || cred.authToken
|
||||
? ' (' + mask(cred.authToken || cred.apiKey) + ')' : ''),
|
||||
credentialFile: cred.credentialFile,
|
||||
credentialFileExists: (() => { try { return fs.existsSync(cred.credentialFile); } catch (e) { return false; } })(),
|
||||
ready: !!(cred.authToken || cred.apiKey),
|
||||
},
|
||||
dify: {
|
||||
url: String(process.env.DIFY_URL || 'https://mydify.youmanvideo.com/v1'),
|
||||
@@ -946,12 +1046,7 @@ const server = http.createServer((req, res) => {
|
||||
if (url.searchParams.get('probe') === '1' && backend === 'cli') {
|
||||
const t0 = Date.now();
|
||||
try {
|
||||
const r = await runCli({
|
||||
prompt: '只回复两个字:成功', cwd: ROOT, model: c.model,
|
||||
apiKey: c.apiKey, authToken: c.authToken,
|
||||
internetEnvironment: c.internetEnvironment, cliPath: c.path,
|
||||
timeoutMs: 120000,
|
||||
});
|
||||
const r = await runCli(cliBaseOpts({ prompt: '只回复两个字:成功', timeoutMs: 120000 }));
|
||||
info.probe = {
|
||||
ok: r.ok, ms: Date.now() - t0,
|
||||
text: (r.text || '').slice(0, 100),
|
||||
@@ -1030,18 +1125,7 @@ const server = http.createServer((req, res) => {
|
||||
// dify = 原链路(下方保留,未改动)
|
||||
// 两条链路对外都产出同一套 SSE 协议:{delta} / {replace} / {error},前端无需感知差异。
|
||||
if (aiBackend() === 'cli') {
|
||||
const c = (CFG.ai && CFG.ai.cli) || {};
|
||||
const base = {
|
||||
prompt: query,
|
||||
cwd: ROOT,
|
||||
model: c.model,
|
||||
apiKey: c.apiKey,
|
||||
authToken: c.authToken,
|
||||
internetEnvironment: c.internetEnvironment,
|
||||
permissionMode: c.permissionMode,
|
||||
cliPath: c.path,
|
||||
timeoutMs: Math.max(30000, parseInt(c.timeoutMs, 10) || 300000),
|
||||
};
|
||||
const base = cliBaseOpts({ prompt: query });
|
||||
if (stream) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream; charset=utf-8',
|
||||
@@ -1063,6 +1147,10 @@ const server = http.createServer((req, res) => {
|
||||
// 已产出的内容优先保留(模型中途失败 / 超时场景)
|
||||
if (acc && r.text && r.text !== acc) write({ replace: r.text });
|
||||
write({ error: r.error || 'CLI 执行失败' });
|
||||
} else if (r.text && r.text !== acc) {
|
||||
// 成功但「流过的内容」≠「终态正文」:典型是模型在联网搜索前先吐了一句过程话术,
|
||||
// 后端已把工具调用前的部分切掉 → 这里用权威正文整体覆盖(前端原生支持 replace)
|
||||
write({ replace: r.text });
|
||||
}
|
||||
log('AI 需求打磨(cli,stream): ' + accountName + ' | ' + acc.length + '字'
|
||||
+ (r.sessionId ? ' | session ' + r.sessionId : '') + (r.ok ? '' : ' | 失败: ' + r.error));
|
||||
@@ -1092,7 +1180,7 @@ const server = http.createServer((req, res) => {
|
||||
const difyResp = await fetch(difyUrl + '/chat-messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ inputs: { is_think: '0', is_online: '0', model: process.env.DIFY_MCN_MODEL || 'gemini' }, query, response_mode: 'streaming', user: 'mcn-work-shop' }),
|
||||
body: JSON.stringify({ inputs: { is_think: '0', is_online: '0', model: process.env.DIFY_MCN_MODEL || 'gemini' }, query, response_mode: 'streaming', user: 'mcn-workshop' }),
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
if (!difyResp.ok || !difyResp.body) {
|
||||
@@ -1171,7 +1259,7 @@ const server = http.createServer((req, res) => {
|
||||
difyResp = await fetch(difyUrl + '/chat-messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ inputs: { is_think: '0', is_online: '0', model: process.env.DIFY_MCN_MODEL || 'gemini' }, query, response_mode: 'blocking', user: 'mcn-work-shop' }),
|
||||
body: JSON.stringify({ inputs: { is_think: '0', is_online: '0', model: process.env.DIFY_MCN_MODEL || 'gemini' }, query, response_mode: 'blocking', user: 'mcn-workshop' }),
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
} finally { clearTimeout(timer); }
|
||||
@@ -1238,14 +1326,14 @@ const server = http.createServer((req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// ---- 会话自动清理:mcn-work-shop 空间超 6 小时无活动的已结束会话自动清除 ----
|
||||
// ---- 会话自动清理:mcn-workshop 空间超 6 小时无活动的已结束会话自动清除 ----
|
||||
// 09-01 用户需求:工作台触发(cwd = 工作台目录 SESSION_CWD)的 AI 会话任务,超过 6 小时自动清除
|
||||
// 09-10 路径治本:cwd 由写死盘符改为 ROOT 动态推导(<技能根>/mcn-work-shop);
|
||||
// 清理模式放宽为 '%mcn-work%',同时覆盖历史会话(旧 mcn-workshop)与新建会话(mcn-work-shop)
|
||||
// 09-10 路径治本:cwd 由写死盘符改为 ROOT 动态推导(<技能根>/mcn-workshop);
|
||||
// 清理模式放宽为 '%mcn-work%',同时覆盖历史会话(早期 D:\AgentSkill\mcn-workshop、迁移期 …\mcn-work-shop)与新建会话(…\V1.0\mcn-workshop)
|
||||
// 判定:cwd 含 mcn-work + status=completed(执行中一律不删)+ 未删过 + 最后活动时间超过 6 小时
|
||||
// 方式:软删除(置 deleted_at),客户端左侧会话栏按 deleted_at 过滤即不再显示;可逆安全
|
||||
const SESSION_TTL_MS = 6 * 3600 * 1000; // 6 小时
|
||||
const SESSION_CWD_PATTERN = '%mcn-work%'; // 兼容 SESSION_CWD(mcn-work-shop)与历史 cwd(mcn-workshop)
|
||||
const SESSION_CWD_PATTERN = '%mcn-work%'; // 覆盖当前 SESSION_CWD(…\V1.0\mcn-workshop)与历史 cwd(早期 D:\AgentSkill\mcn-workshop、迁移期 …\mcn-work-shop)
|
||||
const SESSION_CLEAN_INTERVAL_MS = 60 * 60 * 1000; // 每小时检查一次
|
||||
|
||||
function cleanupExpiredSessions() {
|
||||
@@ -1262,7 +1350,7 @@ function cleanupExpiredSessions() {
|
||||
AND COALESCE(last_activity_at, updated_at, created_at, 0) < ?
|
||||
`).run(now, SESSION_CWD_PATTERN, cutoff);
|
||||
db.close();
|
||||
if (Number(r.changes) > 0) log(`会话自动清理:已清除 mcn-work-shop 超 6 小时无活动的已完成会话 ${r.changes} 条`);
|
||||
if (Number(r.changes) > 0) log(`会话自动清理:已清除 mcn-workshop 超 6 小时无活动的已完成会话 ${r.changes} 条`);
|
||||
} catch (e) {
|
||||
console.error('会话自动清理失败:', e.message);
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -129,11 +129,11 @@
|
||||
|
||||
## 2026-09-10(工作台配置收敛:端口/DB/目录单源化,代码层统一引用 config.json)
|
||||
|
||||
- **触发**:用户问「这些设定能否用配置,所有文件和脚本都引用,避免有调整时到处修改」。确认范围:只收敛代码层(不动文档/技能规范),配置放 `mcn-work-shop/config.json`。
|
||||
- **触发**:用户问「这些设定能否用配置,所有文件和脚本都引用,避免有调整时到处修改」。确认范围:只收敛代码层(不动文档/技能规范),配置放 `mcn-workshop/config.json`。
|
||||
- **目标**:工作台散落在 server.js / dsh-data.js / 4 个辅助脚本 / 前端 data-pages.js / start.bat 里的硬编码设定(端口 8900、DB 名 mcn-plugin.db、产出根 MCNSkill项目、榜单目录),统一收进 config.json,代码层一律从配置读,调整只需改 config.json 一处。
|
||||
- **新增**:
|
||||
- `mcn-work-shop/config.json`:单一配置源(port / dbName / outputRoot / businessDataDir / extraRoots)
|
||||
- `mcn-work-shop/load-config.js`:配置读取唯一入口 + 默认值唯一来源(config 缺失/损坏回退 DEFAULTS)
|
||||
- `mcn-workshop/config.json`:单一配置源(port / dbName / outputRoot / businessDataDir / extraRoots)
|
||||
- `mcn-workshop/load-config.js`:配置读取唯一入口 + 默认值唯一来源(config 缺失/损坏回退 DEFAULTS)
|
||||
- **改动**:
|
||||
- `server.js`:`require('./load-config')` 取 CFG;`PORT_BASE = argv[2] || CFG.port`(argv 端口仍优先);`DEFAULT_ROOT` / `path.join(ROOT, CFG.dbName)` 从 CFG 读;`saveConfig` 合并写盘保留其它字段;错误提示「mcn-plugin.db」→ CFG.dbName 动态
|
||||
- `dsh-data.js`:删本地 loadCfg,改用 loadConfig;`DSH_DB = path.join(__dirname, CFG.dbName)`;榜单目录 `path.join(...CFG.businessDataDir)`
|
||||
@@ -152,7 +152,7 @@
|
||||
- **判定层**:`V1.0/references-add/路径配置.md`、`Lite1.0/references-add/路径配置.md`、`subskills/mcn-dou-analysis/references/路径配置.md`、`subskills/mcn-video-prompt/references-add/路径配置.md`(判定表/执行规则/解析规则/变更记录,v3)
|
||||
- **SKILL 层**:`V1.0/SKILL.md`(榜单读取 71/78、外部技能依赖 dsh 副本删除、产出路径表述)、`Lite1.0/SKILL.md`、`subskills/mcn-dou-analysis/SKILL.md`(42/44/51/52/58/59/82/95 行)、`subskills/mcn-video-prompt/SKILL.md`(502/505/531-539)、`subskills/mcn-data-insight/SKILL.md`(90-96/116 行)
|
||||
- **标准库**:5 份 `references/规则/路径引用规范.md`(「三种机器」→「两种机器」)、`references/创作流程规范.md`、`references/创作流程/{11_脚本检查和诊断,4_账号设定解析和确认}.md`、`references/接口调用/{MCP_工具调用规范,自动化任务清理规范}.md`
|
||||
- **数据源层**:`subskills/mcn-data-insight/scripts/fetch_week_ranks.py`(删 `.dsh` 分支代码)、`mcn-work-shop/dsh-data.js`(删 `isDshDeploy` 分支 + 注释)、`mcn-work-shop/public/data-pages.js`(SKILL_HINT_RANKING + 写库注释)、`subskills/mcn-data-insight/subskills/{douyin-rise-ranking,douyin-weekly-surge,douyin-content-surge,douyin-daily-hot}/SKILL.md`、`references/规则/抖音数据规则.md`
|
||||
- **数据源层**:`subskills/mcn-data-insight/scripts/fetch_week_ranks.py`(删 `.dsh` 分支代码)、`mcn-workshop/dsh-data.js`(删 `isDshDeploy` 分支 + 注释)、`mcn-workshop/public/data-pages.js`(SKILL_HINT_RANKING + 写库注释)、`subskills/mcn-data-insight/subskills/{douyin-rise-ranking,douyin-weekly-surge,douyin-content-surge,douyin-daily-hot}/SKILL.md`、`references/规则/抖音数据规则.md`
|
||||
- **账号分析层**:`references/账号数据分析方法.md`、`references/feature/{01_获取账号信息,03_提炼账号设定,04_分析视频数据,06_生成账号设定卡片}.md`、`references/铁律避坑规则/账号信息获取执行避坑.md`(删「dsh 原库」)、`references/浏览器搜索抖音账号操作规范.md`(159/175 行 browser-harness dsh 部署环境)
|
||||
- **README**:`references-add/README.md`(两处)、`subskills/mcn-dou-analysis/references-add/README.md`、`subskills/mcn-video-prompt/references-add/README.md`、`subskills/mcn-video-prompt/references/README.md`
|
||||
- **残留核对**:全树扫描 `.dsh`/`dshworkspace`/`dsh 部署`/`dsh 环境`/`dsh_MCNProject`/`②dsh`/`③用户环境`——**当前有效规则零残留**,剩余命中全部为豁免三类:①变更记录/变更日志历史行;②「09-10 起 dsh 环境已废弃」说明文字;③`路径引用规范.md` 用户级路径示例 `{主目录}/.dsh/...`(DSH 工具配置目录,非部署环境)。工作台重启验证 HTTP 200,`dsh-data.js`/`fetch_week_ranks.py` 语法校验通过。
|
||||
@@ -172,8 +172,8 @@
|
||||
- **MCP 文档**:`references/接口调用/MCP_工具调用规范.md`、`subskills/mcn-dou-analysis/references/接口调用/MCP_工具调用规范.md`(开发/非开发环境判定)
|
||||
- **Lite1.0**:`SKILL.md` 外部技能依赖段落(见下)
|
||||
- **真实读写路径修复(B 类 8 处)**:
|
||||
- `mcn-work-shop/server.js`:新增 `BROWSER_LOCK_FILE = path.join(ROOT, '.browser-lock')`、`SESSION_CWD = ROOT`;2 处 `const cwd` 与 `BROWSER_LOCK` 改用常量;`SESSION_CWD_PATTERN` 由 `'%mcn-workshop%'` 放宽为 `'%mcn-work%'`(**同时兼容历史会话 mcn-workshop 与新建会话 mcn-work-shop**,避免 6 小时清理对旧会话失效)
|
||||
- `mcn-work-shop/build-create-prompts.cjs` / `submit-review-tasks.cjs`:prompt 内 DB 绝对路径 → 改用文件内既有 `DB` 常量插值
|
||||
- `mcn-workshop/server.js`:新增 `BROWSER_LOCK_FILE = path.join(ROOT, '.browser-lock')`、`SESSION_CWD = ROOT`;2 处 `const cwd` 与 `BROWSER_LOCK` 改用常量;`SESSION_CWD_PATTERN` 由 `'%mcn-workshop%'` 放宽为 `'%mcn-work%'`(**同时兼容历史会话 mcn-workshop 与新建会话 mcn-workshop**,避免 6 小时清理对旧会话失效)
|
||||
- `mcn-workshop/build-create-prompts.cjs` / `submit-review-tasks.cjs`:prompt 内 DB 绝对路径 → 改用文件内既有 `DB` 常量插值
|
||||
- `V1.0/SKILL.md` 第 5 条:文档里的运行时 `cwds=["D:\AgentSkill\mcn-workshop"]` → 改为 `<工作台目录>`(由 server.js 从 ROOT 动态推导)
|
||||
- **Lite1.0 外部技能依赖修正(09-02 结构变更补同步)**:原段落仍写「三环境两步判定 + `D:\AgentSkill\mcn-dou-analysis` 外部路径」,既失效又未同步 09-02「mcn-dou-analysis 并入 `subskills/`」变更。修正为:**Lite1.0 目录下无 `subskills/`(实测),未内置该技能**;需要账号解析改用完整版「短视频工作台」V1.0,或先把技能同步进 `Lite1.0/subskills/` 后按相对路径 `subskills/mcn-dou-analysis/SKILL.md` 调用;禁止盘符绝对路径定位。
|
||||
- **BROWSER_LOCK 契约同步**:`subskills/mcn-dou-analysis/references/浏览器搜索抖音账号操作规范.md` 5.6 节锁文件路径由 8 处盘符硬编码 → 改为相对 `.browser-lock`(工作台触发的会话 cwd 即工作台目录),并注明以工作台注入的绝对路径为准。
|
||||
@@ -188,7 +188,7 @@
|
||||
|
||||
- **D·视觉脚本 → D·视觉短片(全量)**:S9 格式列举(SKILL.md)、S6 内容形式映射表(F04 测评种草/F07 纯视觉短片)、S9 正文 6 处、创作流程规范 3 处、03_框架节奏/07_脚本格式选择指南 8 处——与工作台需求框/用户既定术语对齐,消除「纯视觉短片→D·视觉脚本」混用。
|
||||
- **预设钩子 → 开场钩子(全量)**:S5 选题方案字段(含 10 种开场方式引用)+ S7 大纲回扣语义 + 04_爆款开场/00_口径说明——对外统称「开场钩子」(=S6 场1 段名、工作台需求框 hook 字段同名同义),S5 阶段语义由「预设」改为直接以开场钩子表达。
|
||||
- **需求框-技能衔接注明(mcn-work-shop/prompts/clarify.md)**:内容形式 4 项为粗选方向,落创作时细化为 S6 口径(口播→教程/知识分享/盘点,视觉短片→测评/沉浸式);故事框架四词(钩子前置/悬念反转/递进铺垫/先抑后扬)=达人倾向词,由 S6 用叙事形态 F01-F08 + 结构模板(三段式/五段式)定型。
|
||||
- **需求框-技能衔接注明(mcn-workshop/prompts/clarify.md)**:内容形式 4 项为粗选方向,落创作时细化为 S6 口径(口播→教程/知识分享/盘点,视觉短片→测评/沉浸式);故事框架四词(钩子前置/悬念反转/递进铺垫/先抑后扬)=达人倾向词,由 S6 用叙事形态 F01-F08 + 结构模板(三段式/五段式)定型。
|
||||
- **规则沉淀(09-07)**:新增 references/规则/概念分层与源头表述规范.md(先分层再落笔:策略/实现/容器各写源头正文;禁止 X=Y/括注跨文档打补丁,改源头正文;同族术语对外一个总称、改名一次全链路;含开场钩子家族案例);SKILL.md 红线摘要区新增 🔴 索引行;项目级 技能审计检查清单.md 一致性维新增第 11/12 条(概念分层、源头表述自洽),编号重排 1→15,使用方法补层级先于枚举。
|
||||
- **开场钩子分层表述源头化(5_选题 / 6_框架 / 04_爆款开场·00_)**:三个源头正文自洽——开场方式(04_ 库 10 种标准分类)由 S5 依据用户要求/账号结构性符号/对标拆解(S3)选定并写入选题方案;S6 场次表「本场钩子」=五类留人机制(悬念提问/反差画面/直击痛点/身份共鸣/利益承诺),场1 的本场钩子用于把 S5 已选开场方式实现为镜头与台词。消除 S5「钩子类型」一词双指(表头改「开场方式」、方案字段行改为单一开场方式成分);不再以对照式/括注维护,概念各归其位。
|
||||
|
||||
@@ -228,7 +228,7 @@
|
||||
|
||||
## 2026-09-02(工作台 AI 任务契约与执行铁律)
|
||||
|
||||
- 工作台新增第 7 条「AI 任务入口技能契约(P0)」:所有 AI 会话任务入口须遵守功能入口×技能调用契约(prompt=技能前缀常量+业务正文+skills 参数级挂载),新增/修改入口前先读 `mcn-work-shop/docs/AI会话任务输入输出对照.md`(含新增入口检查清单 7 项)——防裸 prompt 跳步/技能声明混入业务串/BROWSER_HINT 误注入;原第 7/8 条顺延为 8/9。
|
||||
- 工作台新增第 7 条「AI 任务入口技能契约(P0)」:所有 AI 会话任务入口须遵守功能入口×技能调用契约(prompt=技能前缀常量+业务正文+skills 参数级挂载),新增/修改入口前先读 `mcn-workshop/docs/AI会话任务输入输出对照.md`(含新增入口检查清单 7 项)——防裸 prompt 跳步/技能声明混入业务串/BROWSER_HINT 误注入;原第 7/8 条顺延为 8/9。
|
||||
- 工作台任务执行方式铁律固化:创作/改写/复盘必须走 `POST /api/run` + skills 参数,禁会话内手写草稿。
|
||||
- 调度机制实测定稿 + 端口 8899→8900。
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
| 场景 | 说明 |
|
||||
|------|------|
|
||||
| 仅开发机工具 | 不随技能分发的目录(如 `mcn-work-shop/`),内部可保留开发机路径 |
|
||||
| 仅开发机工具 | 不随技能分发的目录(如 `mcn-workshop/`),内部可保留开发机路径 |
|
||||
| 环境判别/路径配置文档 | `路径配置.md`、SKILL.md 环境判定节——锚点 git 检出特征是**规则内容**不是引用,保留 |
|
||||
| 示例/教学文本 | `C:/Videos/demo.mp4`、`C:/path/to/file` 等明确示例(`path/to`、`demo` 字样) |
|
||||
| 历史日志 | `.workbuddy/memory/` 既有日志保留原样,不改写历史 |
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
| 场景 | 说明 |
|
||||
|------|------|
|
||||
| 仅开发机工具 | 不随技能分发的目录(如 `mcn-work-shop/`),内部可保留开发机路径 |
|
||||
| 仅开发机工具 | 不随技能分发的目录(如 `mcn-workshop/`),内部可保留开发机路径 |
|
||||
| 环境判别/路径配置文档 | `路径配置.md`、SKILL.md 环境判定节——锚点 git 检出特征是**规则内容**不是引用,保留 |
|
||||
| 示例/教学文本 | `C:/Videos/demo.mp4`、`C:/path/to/file` 等明确示例(`path/to`、`demo` 字样) |
|
||||
| 历史日志 | `.workbuddy/memory/` 既有日志保留原样,不改写历史 |
|
||||
|
||||
@@ -39,7 +39,7 @@ from urllib.error import HTTPError, URLError
|
||||
LIKES_API = "https://redfox.hk/story/api/dy/search/likesRank"
|
||||
SURGE_API = "https://redfox.hk/story/api/dy/search/hotContentRank"
|
||||
|
||||
# 子目录(与 mcn-work-shop/dsh-data.js rankSubDir 对应)
|
||||
# 子目录(与 mcn-workshop/dsh-data.js rankSubDir 对应)
|
||||
SUB_DIR = {"hot": "视频热榜", "likes": "点赞榜"}
|
||||
# 文件名前缀(与账号周榜 抖音周榜_{赛道}_{周一}.json 区分)
|
||||
FILE_PREFIX = {"hot": "抖音周榜_视频热榜", "likes": "抖音周榜_点赞榜"}
|
||||
@@ -50,7 +50,7 @@ CATEGORY_ALL = "全部"
|
||||
|
||||
|
||||
def resolve_ranking_dir():
|
||||
"""与 mcn-work-shop/dsh-data.js resolveRankingDir 同款:env → 桌面 抖音榜单(09-10 起无 dsh 分支)"""
|
||||
"""与 mcn-workshop/dsh-data.js resolveRankingDir 同款:env → 桌面 抖音榜单(09-10 起无 dsh 分支)"""
|
||||
env = os.environ.get("MCN_RANKING_DIR", "").strip()
|
||||
if env:
|
||||
return env
|
||||
|
||||
@@ -157,7 +157,7 @@ description: "MCN 达人爆款视频解析与人设卡生成技能:达人账
|
||||
核心流程概要:
|
||||
- **触发**:账号详情视频列表「解析视频」按钮 / 用户指定单条视频
|
||||
- **流程**:`upload_douyin_video` 提交(3-5 分钟等待)→ `short_video_detail` 查询(content + analysis)→ `scripts/json_tool.py` 保存到 `{产出根目录}/{达人昵称}/视频对标/{视频文件夹}/`
|
||||
- **可选写库**:① 环境有 MCN 插件接口(dsh)时补录 `account_video_source` 并提取选题;② **工作台环境(mcn-work-shop,本机 `http://localhost:8900`)**:功能三/五解析产物落盘后调用 `POST /api/import/account`(body `{"account":"{达人昵称}"}`)同步入库——扫描账号产出目录写工作台库 `mcn-plugin.db`(`视频分析/*/analysis.json`→`account_video_analysis`、`content.json`→`account_video_source`、`{达人昵称}账号设定.md`→`account_persona`、`{达人昵称}账号数据分析.md`→`account_analysis`、`短视频表格.xlsx`→`account_videos` 补视频列表),供工作台选题(`/api/ai/topics`)基于真实视频解析内容生成。入库实现见工作台 `server.js`(09-01 新增)
|
||||
- **可选写库**:① 环境有 MCN 插件接口(dsh)时补录 `account_video_source` 并提取选题;② **工作台环境(mcn-workshop,本机 `http://localhost:8900`)**:功能三/五解析产物落盘后调用 `POST /api/import/account`(body `{"account":"{达人昵称}"}`)同步入库——扫描账号产出目录写工作台库 `mcn-plugin.db`(`视频分析/*/analysis.json`→`account_video_analysis`、`content.json`→`account_video_source`、`{达人昵称}账号设定.md`→`account_persona`、`{达人昵称}账号数据分析.md`→`account_analysis`、`短视频表格.xlsx`→`account_videos` 补视频列表),供工作台选题(`/api/ai/topics`)基于真实视频解析内容生成。入库实现见工作台 `server.js`(09-01 新增)
|
||||
|
||||
## 八、功能六:生成账号设定卡片
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ PY
|
||||
|
||||
**锁规则(每个需要浏览器的任务必须遵守)**:
|
||||
|
||||
1. **锁文件位置**:**工作台目录下的 `.browser-lock`**(工作台目录 = 本技能根下的 `mcn-work-shop/`)。工作台 `/api/run` 对浏览器类任务会把**运行时解析出的绝对路径**注入 prompt,AI 会话执行时**以注入的路径为准**;此处不写死盘符。
|
||||
1. **锁文件位置**:**工作台目录下的 `.browser-lock`**(工作台目录 = 本技能根下的 `mcn-workshop/`)。工作台 `/api/run` 对浏览器类任务会把**运行时解析出的绝对路径**注入 prompt,AI 会话执行时**以注入的路径为准**;此处不写死盘符。
|
||||
- 工作台触发的会话 cwd 即**工作台目录**,故可直接用相对路径 `.browser-lock`(推荐,跨机器可迁移)
|
||||
- 手工执行(cwd 不是工作台目录)时,先定位工作台目录再拼 `.browser-lock`
|
||||
2. **操作前**:检查锁文件是否存在(`ls`)→ 存在则等 10 秒重试(最多 18 次 ≈ 3 分钟)
|
||||
|
||||
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user