@suifei/dsh-ai-team 0.3.20 → 0.3.22

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.22 — 2026-11-10
4
+
5
+ - 报告路径提取只接受无空格的绝对路径,避免把 Thread 叙述当成报告路径;
6
+ - 六问导入在现有报告上验证为 6/6。
7
+
8
+ ## 0.3.21 — 2026-11-10
9
+
10
+ - 打开 Project 时,如果 Thread 里提到 Human 并登记了 Markdown 报告路径,会从报告的「6. 需 Human 决策的问题清单」提取全部问题;
11
+ - 现有项目无需重新问一遍:升级重启后,§6 六问直接进收件箱并可逐项填写提交;
12
+ - 收件箱决策标题显示问题数(如「需 Human 决策 · 6 问」)。
13
+
3
14
  ## 0.3.20 — 2026-11-10
4
15
 
5
16
  - 收件箱决策项可直接选择/填写并提交,Human 决策会回写 Task Thread 并唤醒 owner;
package/index.js CHANGED
@@ -34,7 +34,7 @@ export const name = 'ai-team';
34
34
  /** The workbench only makes sense where the Web carrier exists. */
35
35
  export const inject = ['webServer', 'permissionPresets', 'sandboxPolicy'];
36
36
 
37
- const VERSION = '0.3.20';
37
+ const VERSION = '0.3.22';
38
38
  const PROJECT_ID = 'prj_main';
39
39
  const HUMAN_ID = 'mem_human';
40
40
 
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Read the Human-decision table from a linked Markdown report.
3
+ *
4
+ * This is deliberately narrow: only a report containing a section titled
5
+ * `6.` / `六` with a table gets converted. The Inbox still points back to the
6
+ * source Thread; the table is exposed so Human does not have to open the report
7
+ * just to discover the questions.
8
+ */
9
+
10
+ function stripMarkdown(value) {
11
+ return String(value ?? '')
12
+ .replace(/\*\*/g, '')
13
+ .replace(/`/g, '')
14
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
15
+ .trim();
16
+ }
17
+
18
+ export function findReportPath(text) {
19
+ if (typeof text !== 'string') return null;
20
+ const matches = text.match(/[A-Za-z]:\\[^\s"'<>|]+?\.md\b/g);
21
+ return matches === null || matches.length === 0 ? null : matches[matches.length - 1];
22
+ }
23
+
24
+ export function parseDecisionQuestions(markdown) {
25
+ const lines = String(markdown ?? '').split(/\r?\n/);
26
+ const start = lines.findIndex((line) => /^##\s*6[.、\s]/.test(line) || /^##\s*6\s*$/.test(line));
27
+ if (start < 0) return [];
28
+ let end = lines.length;
29
+ for (let index = start + 1; index < lines.length; index += 1) {
30
+ if (/^##\s/.test(lines[index]) || /^---\s*$/.test(lines[index])) { end = index; break; }
31
+ }
32
+ const questions = [];
33
+ for (const line of lines.slice(start + 1, end)) {
34
+ if (!line.trim().startsWith('|')) continue;
35
+ const cells = line.split('|').slice(1, -1).map((cell) => cell.trim());
36
+ if (cells.length < 2) continue;
37
+ if (!/^\d+$/.test(stripMarkdown(cells[0]))) continue;
38
+ const text = stripMarkdown(cells[1]);
39
+ if (text === '') continue;
40
+ questions.push({
41
+ id: `q${questions.length + 1}`,
42
+ text,
43
+ ...(cells[2] ? { recommendation: stripMarkdown(cells[2]) } : {}),
44
+ ...(cells[3] ? { blocking: stripMarkdown(cells[3]) } : {}),
45
+ });
46
+ }
47
+ return questions;
48
+ }
@@ -61,7 +61,7 @@ export class ProjectRuntime {
61
61
  });
62
62
  }
63
63
  await seedDefaultRoster(service, projectId, { humanId: HUMAN_ID });
64
- await service.backfillHumanMentions();
64
+ await service.backfillHumanMentions(state.projects?.[projectId]?.workspacePath ?? '');
65
65
  }
66
66
  return runtime;
67
67
  }
@@ -1,4 +1,6 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
1
2
  import { ERRORS, fail } from '../domain/errors.js';
3
+ import { findReportPath, parseDecisionQuestions } from './decision-report.js';
2
4
  import { ids } from '../domain/ids.js';
3
5
  import { ackInboxItem, createInboxItem } from '../domain/inbox.js';
4
6
  import {
@@ -141,7 +143,7 @@ export class TeamService {
141
143
  * that explicitly asked Human to decide. Runs on Project open once; the
142
144
  * per-entry dedupe makes it idempotent.
143
145
  */
144
- async backfillHumanMentions() {
146
+ async backfillHumanMentions(workspacePath = '') {
145
147
  const humanId = this.state.humanMemberId
146
148
  ?? Object.values(this.state.members ?? {}).find((member) => member.kind === 'human')?.id
147
149
  ?? null;
@@ -152,6 +154,20 @@ export class TeamService {
152
154
  .filter((item) => item.memberId === humanId && item.ref?.entrySeq !== undefined)
153
155
  .map((item) => `${item.ref?.taskId}:${item.ref?.entrySeq}`),
154
156
  );
157
+ let reportPath = null;
158
+ for (const task of Object.values(this.state.tasks ?? {})) {
159
+ for (const entry of this.state.threads?.[task.id]?.entries ?? []) {
160
+ const found = findReportPath(entry.body);
161
+ if (found !== null && existsSync(found)) { reportPath = found; break; }
162
+ }
163
+ if (reportPath !== null) break;
164
+ }
165
+ let reportQuestions = [];
166
+ if (reportPath !== null) {
167
+ try {
168
+ reportQuestions = parseDecisionQuestions(readFileSync(reportPath, 'utf8'));
169
+ } catch { /* report may be unavailable; keep the Thread fallback */ }
170
+ }
155
171
  let created = 0;
156
172
  for (const task of Object.values(this.state.tasks ?? {})) {
157
173
  if (task.tombstone === true) continue;
@@ -161,6 +177,7 @@ export class TeamService {
161
177
  const key = `${task.id}:${entry.seq}`;
162
178
  if (existing.has(key)) continue;
163
179
  existing.add(key);
180
+ const useQuestions = reportQuestions.length > 0;
164
181
  const item = createInboxItem({
165
182
  memberId: humanId,
166
183
  type: 'decision_required',
@@ -169,7 +186,8 @@ export class TeamService {
169
186
  entrySeq: entry.seq,
170
187
  memberId: entry.authorId,
171
188
  category: 'thread_mention',
172
- title: String(entry.body).slice(0, 180),
189
+ title: useQuestions ? `需 Human 决策 · ${reportQuestions.length} 问` : String(entry.body).slice(0, 180),
190
+ ...(useQuestions ? { questions: reportQuestions, source: reportPath } : {}),
173
191
  },
174
192
  requiresDecision: true,
175
193
  }, { at: this.now() });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suifei/dsh-ai-team",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
4
4
  "type": "module",
5
5
  "description": "文渊阁 (Wenyuan Pavilion) — AI Team mode for DeepSeek Harness: concurrent projects, owned tasks, threads, inbox and an append-only team ledger",
6
6
  "license": "MIT",