dsh-task-ledger 0.0.62 → 0.0.64

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.
@@ -5,19 +5,38 @@ export type LedgerScriptResult<T> = {
5
5
  ok: false;
6
6
  error: string;
7
7
  };
8
+ /** 写命令的返回:只收 stdout 原文(claim / set 打的是人话,不是 JSON)。 */
9
+ export type LedgerCommandResult = {
10
+ ok: true;
11
+ output: string;
12
+ } | {
13
+ ok: false;
14
+ error: string;
15
+ };
16
+ export interface RunOptions {
17
+ /** 测试用:覆盖脚本路径。 */
18
+ scriptPath?: string;
19
+ /** 覆盖超时。 */
20
+ timeoutMs?: number;
21
+ /**
22
+ * 子进程 cwd。写命令(claim / set)必须落在工作区:
23
+ * 脚本内的 owner 取 `git config user.name`,走进程 cwd 而不是 `--repo`。
24
+ */
25
+ cwd?: string;
26
+ }
8
27
  /**
9
- * 全插件唯一碰 Python 的地方:spawn python3 跑内嵌 skill 脚本,收 stdout JSON。
28
+ * 读命令:spawn python3 跑内嵌 skill 脚本,收 stdout JSON。
10
29
  * 不解析任何台账格式——解析真源只有 skill 脚本一份(2026-10-17 用户拍板)。
11
30
  *
12
- * 出错一律归一为 { ok:false, error }(人话,直接可上界面):
13
- * 起不了 python3 / 超时(kill)/ 非零退出(stderr 原文)/ 输出不是 JSON。
14
- *
15
31
  * @param sub - 子命令(list / history / show…)
16
32
  * @param args - 参数数组,由调用方自拼(含 --repo / --json / --months 等)
17
33
  * @param opts.scriptPath - 测试用:覆盖脚本路径
18
34
  * @param opts.timeoutMs - 覆盖超时
35
+ * @param opts.cwd - 子进程工作目录(写命令取 git 身份用)
19
36
  */
20
- export declare function runLedgerScript<T = unknown>(sub: string, args: readonly string[], opts?: {
21
- scriptPath?: string;
22
- timeoutMs?: number;
23
- }): Promise<LedgerScriptResult<T>>;
37
+ export declare function runLedgerScript<T = unknown>(sub: string, args: readonly string[], opts?: RunOptions): Promise<LedgerScriptResult<T>>;
38
+ /**
39
+ * 写命令(claim / set):同样是 spawn python3,但**不解析 JSON**——这些子命令打的是人话,
40
+ * 成功与否看退出码,失败时 stderr 就是能直接上界面的人话。
41
+ */
42
+ export declare function runLedgerCommand(sub: string, args: readonly string[], opts?: RunOptions): Promise<LedgerCommandResult>;
@@ -9,22 +9,14 @@ const DEFAULT_SCRIPT_PATH = fileURLToPath(new URL('../../../skill/scripts/progre
9
9
  /** 单次脚本调用超时上限(ms)。实测单仓 5 万行全量 1.2s,10s 是宽松上限。 */
10
10
  const DEFAULT_TIMEOUT_MS = 10_000;
11
11
  /**
12
- * 全插件唯一碰 Python 的地方:spawn python3 跑内嵌 skill 脚本,收 stdout JSON。
13
- * 不解析任何台账格式——解析真源只有 skill 脚本一份(2026-10-17 用户拍板)。
14
- *
15
- * 出错一律归一为 { ok:false, error }(人话,直接可上界面):
16
- * 起不了 python3 / 超时(kill)/ 非零退出(stderr 原文)/ 输出不是 JSON。
17
- *
18
- * @param sub - 子命令(list / history / show…)
19
- * @param args - 参数数组,由调用方自拼(含 --repo / --json / --months 等)
20
- * @param opts.scriptPath - 测试用:覆盖脚本路径
21
- * @param opts.timeoutMs - 覆盖超时
12
+ * 起一次 python3、收 stdout / stderr。出错一律归一成人话(起不了 python3 / 超时 / 非零退出)。
13
+ * 解析留给调用方:读命令要 JSON,写命令要原文。
22
14
  */
23
- export function runLedgerScript(sub, args, opts) {
15
+ function exec(sub, args, opts) {
24
16
  const scriptPath = opts?.scriptPath ?? DEFAULT_SCRIPT_PATH;
25
17
  const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
26
18
  return new Promise((resolve) => {
27
- const child = spawn('python3', [scriptPath, sub, ...args], { stdio: ['ignore', 'pipe', 'pipe'] });
19
+ const child = spawn('python3', [scriptPath, sub, ...args], { cwd: opts?.cwd, stdio: ['ignore', 'pipe', 'pipe'] });
28
20
  let stdout = '';
29
21
  let stderr = '';
30
22
  let settled = false;
@@ -49,12 +41,36 @@ export function runLedgerScript(sub, args, opts) {
49
41
  settle({ ok: false, error: stderr.trim() || `skill 脚本退出码 ${String(code)}` });
50
42
  return;
51
43
  }
52
- try {
53
- settle({ ok: true, data: JSON.parse(stdout) });
54
- }
55
- catch {
56
- settle({ ok: false, error: `skill 脚本输出不是 JSON:${stdout.slice(0, 200)}` });
57
- }
44
+ settle({ ok: true, stdout });
58
45
  });
59
46
  });
60
47
  }
48
+ /**
49
+ * 读命令:spawn python3 跑内嵌 skill 脚本,收 stdout JSON。
50
+ * 不解析任何台账格式——解析真源只有 skill 脚本一份(2026-10-17 用户拍板)。
51
+ *
52
+ * @param sub - 子命令(list / history / show…)
53
+ * @param args - 参数数组,由调用方自拼(含 --repo / --json / --months 等)
54
+ * @param opts.scriptPath - 测试用:覆盖脚本路径
55
+ * @param opts.timeoutMs - 覆盖超时
56
+ * @param opts.cwd - 子进程工作目录(写命令取 git 身份用)
57
+ */
58
+ export async function runLedgerScript(sub, args, opts) {
59
+ const result = await exec(sub, args, opts);
60
+ if (!result.ok)
61
+ return result;
62
+ try {
63
+ return { ok: true, data: JSON.parse(result.stdout) };
64
+ }
65
+ catch {
66
+ return { ok: false, error: `skill 脚本输出不是 JSON:${result.stdout.slice(0, 200)}` };
67
+ }
68
+ }
69
+ /**
70
+ * 写命令(claim / set):同样是 spawn python3,但**不解析 JSON**——这些子命令打的是人话,
71
+ * 成功与否看退出码,失败时 stderr 就是能直接上界面的人话。
72
+ */
73
+ export async function runLedgerCommand(sub, args, opts) {
74
+ const result = await exec(sub, args, opts);
75
+ return result.ok ? { ok: true, output: result.stdout.trim() } : result;
76
+ }
@@ -23,6 +23,13 @@ export interface RouteDeps {
23
23
  readonly registry: WorkspaceRegistryLike;
24
24
  }
25
25
  export declare function registerRoutes(webServer: WebServerLike, deps: RouteDeps): void;
26
+ /**
27
+ * 注册认领 / 释放端点(2026-10-18 B 混合制:没有判断量的动作由 host 直跑脚本)。
28
+ *
29
+ * 这两条**会改磁盘上的台账文件**,所以和 `/close` 一样必须过同源守卫;
30
+ * 成功后就地清该工作区读缓存,避免按钮点完还看到上一屏的 owner。
31
+ */
32
+ export declare function registerWriteRoutes(webServer: WebServerLike, deps: RouteDeps): void;
26
33
  export interface SessionRouteDeps {
27
34
  readonly registry: WorkspaceRegistryLike;
28
35
  /** 起会话的服务面;宿主缺就回 503 人话(端点照常注册,前端不用按版本分支)。 */
@@ -1,6 +1,7 @@
1
1
  import { getHomeAggregate, getFilterOptions, queryClosed } from './board.js';
2
2
  import { getSnapshot } from './services/home.service.js';
3
3
  import { getTrend } from './services/trend.service.js';
4
+ import { claimTask, releaseTask } from './services/ledger-write.service.js';
4
5
  import { getTask, readTasks, readWorklog } from './repositories/ledger.repository.js';
5
6
  import { invalidate } from './repositories/cache.js';
6
7
  import { HandoffError, startCloseSession, } from './services/session.service.js';
@@ -155,6 +156,63 @@ export function registerRoutes(webServer, deps) {
155
156
  }
156
157
  });
157
158
  }
159
+ // ───────────────────────── 台账写端点(host 直写 · 不开会话) ─────────────────────────
160
+ /**
161
+ * 注册认领 / 释放端点(2026-10-18 B 混合制:没有判断量的动作由 host 直跑脚本)。
162
+ *
163
+ * 这两条**会改磁盘上的台账文件**,所以和 `/close` 一样必须过同源守卫;
164
+ * 成功后就地清该工作区读缓存,避免按钮点完还看到上一屏的 owner。
165
+ */
166
+ export function registerWriteRoutes(webServer, deps) {
167
+ const registerWrite = (path, action) => {
168
+ webServer.register({
169
+ kind: 'exact',
170
+ path: `${API_PREFIX}${path}`,
171
+ handler: async (req, res) => {
172
+ if (req.method !== 'POST') {
173
+ sendJson(res, 405, { error: '只支持 POST' });
174
+ return;
175
+ }
176
+ if (!isTrustedRequest(req)) {
177
+ sendJson(res, 403, { error: '只接受同源请求' });
178
+ return;
179
+ }
180
+ let body;
181
+ try {
182
+ body = await readJsonBody(req);
183
+ }
184
+ catch (error) {
185
+ const tooLarge = error instanceof Error && error.message === 'body-too-large';
186
+ sendJson(res, tooLarge ? 413 : 400, { error: tooLarge ? '请求体过大' : '请求体不是合法 JSON' });
187
+ return;
188
+ }
189
+ const fields = typeof body === 'object' && body !== null ? body : {};
190
+ const str = (key) => (typeof fields[key] === 'string' ? fields[key] : '');
191
+ const workspaceId = str('ws');
192
+ const taskId = str('id');
193
+ if (workspaceId === '' || taskId === '') {
194
+ sendJson(res, 400, { error: '缺少 ws / id 参数' });
195
+ return;
196
+ }
197
+ const workspace = workspacesOf(deps).find((item) => item.id === workspaceId);
198
+ if (workspace === undefined) {
199
+ sendJson(res, 404, { error: '工作区不存在或不含台账' });
200
+ return;
201
+ }
202
+ try {
203
+ await action(workspace, taskId);
204
+ invalidate(workspace.id);
205
+ sendJson(res, 200, { ok: true });
206
+ }
207
+ catch (error) {
208
+ sendJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
209
+ }
210
+ },
211
+ });
212
+ };
213
+ registerWrite('/claim', claimTask);
214
+ registerWrite('/release', releaseTask);
215
+ }
158
216
  // ───────────────────────── 会话交接(按钮侧 · 写类端点) ─────────────────────────
159
217
  /** 写端点请求体上限:只收一小段用户留言,别让人往这塞文件。 */
160
218
  const BODY_LIMIT = 64 * 1024;
@@ -0,0 +1,5 @@
1
+ import type { LedgerWorkspace } from '../types.js';
2
+ /** 认领:只有 open 且无人认领才能接(脚本内判定),owner = 当前工作区的 git 身份。 */
3
+ export declare function claimTask(workspace: LedgerWorkspace, taskId: string): Promise<void>;
4
+ /** 释放:退回待认领(owner / started 清空),只许当前 owner 本人(脚本内判定)。 */
5
+ export declare function releaseTask(workspace: LedgerWorkspace, taskId: string): Promise<void>;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 台账写方法(2026-10-18 拍板 B 混合制):认领 / 释放没有判断量,host 直跑 skill 脚本,**不开会话**。
3
+ *
4
+ * 唯一要点:子进程 cwd 必须是工作区 —— `progress.py` 的 owner 取 `git config user.name`,
5
+ * 走的是进程 cwd,`--repo` 只解决路径、不解决身份(2026-10-18 核实结论)。
6
+ *
7
+ * 校验全在脚本里(认领要求 open 且无 owner;释放要求是当前 owner),本层不重复实现。
8
+ */
9
+ import { runLedgerCommand } from '../repositories/runner.js';
10
+ async function run(workspace, sub, args) {
11
+ const result = await runLedgerCommand(sub, [...args, '--repo', workspace.path], { cwd: workspace.path });
12
+ if (!result.ok)
13
+ throw new Error(`${workspace.title}:${result.error}`);
14
+ }
15
+ /** 认领:只有 open 且无人认领才能接(脚本内判定),owner = 当前工作区的 git 身份。 */
16
+ export function claimTask(workspace, taskId) {
17
+ return run(workspace, 'claim', ['--id', taskId]);
18
+ }
19
+ /** 释放:退回待认领(owner / started 清空),只许当前 owner 本人(脚本内判定)。 */
20
+ export function releaseTask(workspace, taskId) {
21
+ return run(workspace, 'set', ['--id', taskId, '--status', 'open']);
22
+ }
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * 满足不了的依赖会令 entry 一直 pending、卡死整个 dsh 启动);两条注入线
10
10
  * 互相独立——skills 缺席不挡数据面,反之亦然。
11
11
  */
12
- import { registerRoutes, registerSessionRoutes } from './host/routes.js';
12
+ import { registerRoutes, registerSessionRoutes, registerWriteRoutes } from './host/routes.js';
13
13
  import { buildSkillRegistration } from './host/skill/registration.js';
14
14
  export const name = 'dsh-task-ledger';
15
15
  /**
@@ -35,7 +35,9 @@ export function apply(ctx) {
35
35
  return;
36
36
  }
37
37
  registerRoutes(webServer, { registry });
38
- console.info('[task-ledger] 数据面路由已注册(/api/task-ledger/*)');
38
+ // 写端点(认领 / 释放):B 混合制下由 host 直跑脚本,不依赖 agents ⇒ 与数据面同一条注入线。
39
+ registerWriteRoutes(webServer, { registry });
40
+ console.info('[task-ledger] 数据面路由已注册(读 /api/task-ledger/* + 写 /claim /release)');
39
41
  });
40
42
  // ── 会话交接(按钮侧「结束任务」,2026-10-18 拍板 B 案:只有结束走会话) ──
41
43
  // 单独一条注入线:agents 缺席只影响这一个端点,不该拖累数据面(上一条线只等 registry + webServer)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-task-ledger",
3
- "version": "0.0.62",
3
+ "version": "0.0.64",
4
4
  "description": "Task ledger board for dsh (DeepSeek Harness): read-only board over task-ledger skill workspaces. The task-ledger skill is bundled — install the plugin, get the skill globally.",
5
5
  "keywords": [
6
6
  "deepseek",