jianzi-content-workbench 0.0.0-stage → 0.2.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,64 @@
1
- # Temporary Holding Version
1
+ # 见字 DSH 内容工作台
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 见字的 DSH 插件面板:把“准备 → 选题 → 文章 → 排版封面 → 图文”五段流程接进 DSH 原生会话。面板不嵌入见字网页,也不直接访问见字 API;它只准备任务草稿,由 Agent 加载项目技能、通过 MCP 工具操作见字。
4
+
5
+ ## 五段流程
6
+
7
+ | 阶段 | 面板动作 | 对应技能 / 门禁 |
8
+ | --- | --- | --- |
9
+ | 01 准备 | 检查环境 | `jianzi-workbench-setup`:工作台在线、RSSHub、账号画像、采集源;RSSHub 未装时给出手动步骤 |
10
+ | 02 发现 | 准备选题研判 | `jianzi-topic-to-article`:批次、采集、打标、事件与事实卡、研判管线;产出排序并停下等用户选 |
11
+ | 03 写作 | 准备文章草稿 | 同技能文章阶段:抓原文、确认角度、保存 draft;不保存 final |
12
+ | 04 交付 | 准备排版交付 | `jianzi-typeset-cover`:只接受已确认终稿,生成 HTML 排版与封面,返回两个入口 |
13
+ | 05 图文 | 准备图文 | `jianzi-social-cards`:工具/事件候选、故事板、锁定、视觉提交 |
14
+
15
+ 面板输入只有三项:补充要求、批次 ID(可选)、候选 ID(可选)。留空时由 Agent 先列出现有批次或候选。任务以未发送草稿的形式进入原生会话,用户检查后按发送才执行。工作台未连接时,状态卡会提供「准备启动任务」按钮;提示词要求 Agent 使用当前 DSH 会话选中的工作空间绝对路径作为 `projectRoot`,调用 `jianzi_workbench_start` 启动,并等待服务就绪和项目指纹校验。若本机没有有效项目目录,检查提示会先询问是否克隆到空目录,确认后才初始化;启动工具本身不会克隆或安装项目。
16
+
17
+ ## 实现
18
+
19
+ - `lib/client.js`:官方 Client Slots(`sidebar.panellist` + `main`)注册侧栏入口和整页面板;点击按钮调用 `uiWorkspace.startSession` 写入任务草稿。
20
+ - `cordis.patch.yml`:用官方 `@deepseek-ai/dsh-mcp-client` 以 stdio 启动包内 `mcp-server.mjs`,DSH 暴露 `mcp__jianzi_workbench__jianzi_*` 工具。
21
+ - `mcp-server.mjs` + `shared/`:复用 `scripts/agent-integration/` 的工具定义、执行器与本机 API 客户端,默认 `http://127.0.0.1:4317`(`JIANZI_WORKBENCH_URL` 可覆盖)。
22
+ - DSH 中调用 `jianzi_workbench_start` 可启动已停止的本机项目并等待健康检查;通过 `projectRoot` 参数或 MCP 进程环境变量 `JIANZI_PROJECT_ROOT` 指定源码项目绝对路径。启动器复用项目内官方启动脚本,不会打开浏览器,并并行轮询就绪接口;服务有效响应后立即核对健康信息与项目指纹,避免启动器进程未退出造成误报。启动最多等待 90 秒,DSH MCP 单次调用上限为 120 秒。
23
+ - 面板文案不硬编码 MCP 工具名,也不复述流程细节;流程以项目技能为准,改流程只改技能。
24
+
25
+ ## 面板读取的数据
26
+
27
+ 面板不能调用 MCP 工具(那些注册在 Host 的模型工具表里)。读取能力由 Host 插件用同一套实现提供:
28
+
29
+ - `GET /jianzi/status` → `jianzi_doctor`(工作台连通、RSSHub、GitHub 状态)
30
+ - `GET /jianzi/batches?limit=8` → `jianzi_batches_list`
31
+ - `GET /jianzi/candidates?batch=<id>&track=article` → `jianzi_batch_candidates`(缺 batch 返回 400;`track=social_cards` 时读取图文候选)
32
+ - `GET /jianzi/social-ranking?batch=<id>` → `jianzi_batch_social_ranking`(图文选题、G_social、素材类型)
33
+ - `GET /jianzi/jobs?batch=<id>&limit=8` → `/api/jobs`(批次采集与后台任务执行情况)
34
+ - `GET /jianzi/calendar?month=YYYY-MM` → `jianzi_calendar_list`(内容日历:文章、已交付图文与写作计划)
35
+
36
+ DSH Agent 还可直接通过 MCP 工具 `jianzi_calendar_list` 读取内容日历,包含终稿文章、已交付图文和写作计划;可传 `month`(`YYYY-MM`)筛选月份。它复用见字现有 `/api/calendar` 查询,不修改日历。
37
+
38
+ Host 插件(`dsh/index.mjs`)通过官方 `ctx.webServer.register` 注册读取路由,供面板同源 `fetch` 使用;项目启动则通过 DSH 原生会话提示词调用 MCP 工具,不经过 Host 请求响应解析。提示词要求 Agent 从当前会话工作空间取得绝对路径并传给工具;MCP 子进程自身运行在 DSH profile 目录,不能用它的当前目录推断项目路径。
39
+
40
+ 面板不再提供手填 ID 的输入框:点顶部「最近批次」chips 选择/取消批次,点「文章选题」列表选择/取消候选,顶部只显示当前选择并可一键清除。
41
+
42
+ 选中批次后,面板下方的「选题与执行」区会显示三栏:批次状态与最近任务(`/jianzi/jobs`)、文章选题(按 F 分,点击填入候选 ID)、图文选题(按 G_social,标注工具/事件图文与资格状态)。
43
+
44
+ 再往下是「内容日历」:按月展示文章、图文与写作计划,按日期分组并统计数量,可用 ‹ › 切换月份。
45
+
46
+ ## 构建与安装
47
+
48
+ ```sh
49
+ node agent-integrations/dsh-workbench/build.mjs
50
+ cd agent-integrations/dsh-workbench
51
+ pnpm pack
52
+ ```
53
+
54
+ 在官方 DeepSeek Harness 桌面应用([deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness))的「插件 → 添加插件」选择生成的 `.tgz`,安装后在已安装插件中启用见字内容工作台。`@deepseek-ai/dsh-mcp-client` 与 `@deepseek-ai/schemastery` 由 Harness 提供,声明为 `peerDependencies`。如安装器提示需重启,再按提示重启或重新加载 DSH。
55
+
56
+ 当前插件可从源码构建 `.tgz` 并本地安装;npm 包尚未发布,也没有发布供用户添加的 DSH 插件索引条目。官方 Harness 支持通过插件管理器安装包名、Git 仓库、tarball 或本地路径;本仓库插件位于 monorepo 子目录,现阶段推荐按上述步骤本地打包安装。详见官方[插件打包与发布说明](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md)及[插件索引说明](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/plugin-market.md)。
57
+
58
+ MCP 服务入口路径在 patch 中按 profile 写死(当前 `profiles/desktop`);在其他 profile 安装时需要同步修改。
59
+
60
+ ## npm 正式发布
61
+
62
+ 正式发布由仓库的 `Publish DSH plugin to npm` GitHub Actions 工作流执行。将插件 `package.json` 的版本更新并合入主分支后,推送对应标签 `dsh-plugin-v<版本号>`(例如 `dsh-plugin-v0.2.29`)。工作流会核对标签和包版本、构建、生成 `.tgz`、上传 GitHub Actions 产物,并将同一个 `.tgz` 发布到 npm。不要用普通 CI 提交触发发布。
63
+
64
+ 首次版本已由维护者本地发布到 npm Registry。后续在该 npm 包设置的 **Trusted Publisher** 中配置 GitHub Actions:用户 `shiker1996`、仓库 `wechat-editroom`、工作流文件名 `publish-dsh-plugin.yml`,并允许直接发布。配置完成后,由上述标签触发的发布工作流将通过 OIDC 发布,无需长期 npm 发布令牌。
@@ -0,0 +1,14 @@
1
+ - insert:
2
+ - id: jianzi-mcp-client
3
+ name: '@deepseek-ai/dsh-mcp-client'
4
+ config:
5
+ serverName: jianzi_workbench
6
+ transport: stdio
7
+ command: node
8
+ args:
9
+ - !!js dshHomePath('profiles/desktop/node_modules/jianzi-content-workbench/mcp-server.mjs')
10
+ cwd: !!js dshHomePath('profiles/desktop')
11
+ toolCallTimeoutMs: 120000
12
+ - id: jianzi-content-workbench
13
+ name: jianzi-content-workbench
14
+ config: {}
package/dsh/index.mjs ADDED
@@ -0,0 +1,123 @@
1
+ import { createJianziClient } from '../shared/client.mjs';
2
+ import { executeJianziTool } from '../shared/tools.mjs';
3
+
4
+ export const name = 'jianzi-content-workbench';
5
+
6
+ const json = (payload, status = 200) => new Response(JSON.stringify(payload), {
7
+ status,
8
+ headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' },
9
+ });
10
+
11
+ const failed = (error) => {
12
+ console.error('Jianzi workbench route failure', error);
13
+ return {
14
+ ok: false,
15
+ error: { code: error?.code || 'JIANZI_READ_FAILED', message: 'Request failed' },
16
+ };
17
+ };
18
+
19
+ // The Client panel cannot call MCP tools: those belong to the model-facing tool
20
+ // registry. It reads Jianzi data through read-only Host routes. Startup actions
21
+ // are prepared as native DSH prompts so the Agent invokes the MCP tool.
22
+ export function apply(ctx) {
23
+ let server;
24
+ try {
25
+ server = typeof ctx.get === 'function' ? ctx.get('webServer') : undefined;
26
+ } catch {
27
+ server = undefined;
28
+ }
29
+ if (!server || typeof server.register !== 'function') return;
30
+
31
+ const client = createJianziClient();
32
+ const read = async (tool, args) => {
33
+ try {
34
+ return { ok: true, data: await executeJianziTool(client, tool, args || {}) };
35
+ } catch (error) {
36
+ return failed(error);
37
+ }
38
+ };
39
+
40
+ ctx.effect(() => server.register({
41
+ kind: 'exact',
42
+ path: '/jianzi/status',
43
+ resource: async () => json(await read('jianzi_doctor')),
44
+ }), 'jianzi: status route');
45
+
46
+ ctx.effect(() => server.register({
47
+ kind: 'exact',
48
+ path: '/jianzi/batches',
49
+ resource: async (request) => {
50
+ const url = new URL(request.url);
51
+ const limit = Math.min(20, Math.max(1, Number(url.searchParams.get('limit')) || 8));
52
+ return json(await read('jianzi_batches_list', { limit }));
53
+ },
54
+ }), 'jianzi: batches route');
55
+
56
+ ctx.effect(() => server.register({
57
+ kind: 'exact',
58
+ path: '/jianzi/candidates',
59
+ resource: async (request) => {
60
+ const url = new URL(request.url);
61
+ const batchId = String(url.searchParams.get('batch') || '').trim();
62
+ if (!batchId) {
63
+ return json({ ok: false, error: { code: 'BATCH_REQUIRED', message: 'batch query parameter is required' } }, 400);
64
+ }
65
+ const track = url.searchParams.get('track') === 'social_cards' ? 'social_cards' : 'article';
66
+ return json(await read('jianzi_batch_candidates', { batchId, track, kind: 'all' }));
67
+ },
68
+ }), 'jianzi: candidates route');
69
+
70
+ ctx.effect(() => server.register({
71
+ kind: 'exact',
72
+ path: '/jianzi/social-ranking',
73
+ resource: async (request) => {
74
+ const url = new URL(request.url);
75
+ const batchId = String(url.searchParams.get('batch') || '').trim();
76
+ if (!batchId) {
77
+ return json({ ok: false, error: { code: 'BATCH_REQUIRED', message: 'batch query parameter is required' } }, 400);
78
+ }
79
+ return json(await read('jianzi_batch_social_ranking', { batchId }));
80
+ },
81
+ }), 'jianzi: social ranking route');
82
+
83
+ ctx.effect(() => server.register({
84
+ kind: 'exact',
85
+ path: '/jianzi/jobs',
86
+ resource: async (request) => {
87
+ const url = new URL(request.url);
88
+ const limit = Math.min(50, Math.max(1, Number(url.searchParams.get('limit')) || 12));
89
+ const batchId = String(url.searchParams.get('batch') || '').trim();
90
+ try {
91
+ const rows = await client.get(`/api/jobs?limit=${limit}`);
92
+ const list = Array.isArray(rows) ? rows : [];
93
+ return json({ ok: true, data: batchId ? list.filter((row) => row.batch_id === batchId) : list });
94
+ } catch (error) {
95
+ return json(failed(error));
96
+ }
97
+ },
98
+ }), 'jianzi: jobs route');
99
+
100
+ ctx.effect(() => server.register({
101
+ kind: 'exact',
102
+ path: '/jianzi/calendar',
103
+ resource: async (request) => {
104
+ const url = new URL(request.url);
105
+ const month = String(url.searchParams.get('month') || '').trim();
106
+ const batchId = String(url.searchParams.get('batch') || '').trim();
107
+ const result = await read('jianzi_calendar_list', month ? { month } : {});
108
+ if (!result.ok || !Array.isArray(result.data)) return json(result);
109
+ const baseUrl = String(client.baseUrl || 'http://127.0.0.1:4317').replace(/\/+$/, '');
110
+ const data = result.data.filter((entry) => !batchId || String(entry?.batch_id || '') === batchId).map((entry) => {
111
+ const id = Number(entry?.id);
112
+ if (!Number.isSafeInteger(id) || id < 1) return entry;
113
+ const path = entry.content_type === 'article'
114
+ ? `/api/documents/${id}/content`
115
+ : entry.content_type === 'social_cards'
116
+ ? `/api/artifacts/${id}/preview`
117
+ : '';
118
+ return path ? { ...entry, previewUrl: `${baseUrl}${path}` } : entry;
119
+ });
120
+ return json({ ...result, data });
121
+ },
122
+ }), 'jianzi: calendar route');
123
+ }
package/icon.svg ADDED
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
2
+ <title>见字品牌印章</title>
3
+ <rect width="1024" height="1024" fill="#1f2d38"/>
4
+ <rect x="96" y="96" width="832" height="832" fill="#aa5b55"/>
5
+ <text x="512" y="676" text-anchor="middle" fill="#fffaf2" font-family="Microsoft YaHei, Noto Sans CJK SC, sans-serif" font-size="560" font-weight="700">见</text>
6
+ </svg>