@suifei/dsh-ai-team 0.3.14 → 0.3.15

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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.15 — 2026-11-10
4
+
5
+ - 成员 Session 自动挂到 Project workspace,不再散落在 DSH「未分组」;
6
+ - Task Thread 增加「对话」按钮,直接打开 assignee/owner 的真实 Session;
7
+ - 成员行增加直接对话按钮;点名字进入成员任务视图;
8
+ - 团队动态只在有 active Task 时显示(或全新项目刚唤醒时),已完成不再显示“正在拆解”;
9
+ - `huashu-nuwa` 从 Team 技能目录、prompt catalog 与 notes 加载中排除。
10
+
3
11
  ## 0.3.14 — 2026-11-10
4
12
 
5
13
  - 主对话顶部增加「团队动态」:显示正在推进的 Task、owner 和运行中的成员;
package/README.md CHANGED
@@ -215,7 +215,7 @@ round-19 DeepSeek + Zhipu 双 provider
215
215
  - 单机、单 Human;不支持多 Human 共享 Team;
216
216
  - billing/网络注入是测试 adapter,不是物理断网;
217
217
  - SSE 分桶用两个真实 provider 的独立订阅端验证,不是两个浏览器窗口;
218
- - nuwa / distilly / darwin 不内置,有则用、无则不用;
218
+ - distilly / darwin 不内置,有则用、无则不用;`huashu-nuwa` 已被 Team 技能目录排除。
219
219
  - desktop profile 由插件 patch 固定为 `danger-full-access / never`,只应在可信机器使用。
220
220
 
221
221
  ---
package/client.js CHANGED
@@ -42,6 +42,7 @@ window.__ModuleLoader__.load({
42
42
  enterHint: 'Ctrl/⌘ + Enter 发送',
43
43
  thread: '讨论',
44
44
  closeThread: '关闭讨论',
45
+ threadChat: '对话',
45
46
  follow: '关注',
46
47
  unfollow: '取消关注',
47
48
  convert: '转为 Task',
@@ -342,6 +343,7 @@ window.__ModuleLoader__.load({
342
343
  enterHint: 'Ctrl/⌘ + Enter to send',
343
344
  thread: 'Discussion',
344
345
  closeThread: 'Close discussion',
346
+ threadChat: 'Chat',
345
347
  follow: 'Follow',
346
348
  unfollow: 'Unfollow',
347
349
  convert: 'Turn into task',
@@ -956,8 +958,9 @@ window.__ModuleLoader__.load({
956
958
  .at-btn-small { min-height: 24px; padding: 0 8px; font-size: 11px; }
957
959
  .at-agentconfig { min-width: 0; display: flex; gap: 4px; align-items: center; color: var(--dsw-alias-label-tertiary); font-size: 10.5px; line-height: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
958
960
  .at-agentfail { flex: none; color: var(--dsw-alias-state-warn-primary); }
959
- .at-agentmore { flex: 0 0 auto; width: 28px; height: 28px; border: 0; border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-tertiary); font: inherit; font-size: 13px; cursor: pointer; }
960
- .at-agentmore:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
961
+ .at-agentmore, .at-agentchat { flex: 0 0 auto; width: 28px; height: 28px; border: 0; border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-tertiary); font: inherit; font-size: 13px; cursor: pointer; }
962
+ .at-agentmore:hover, .at-agentchat:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
963
+ .at-agentchat { display: inline-flex; align-items: center; justify-content: center; }
961
964
  .at-teampulse { width: 100%; max-width: 760px; margin: 4px auto 10px; padding: 10px 12px; border: 1px solid var(--dsw-alias-state-business-primary); border-radius: var(--dsw-radius-md); background: var(--dsw-alias-bg-module-platform); }
962
965
  .at-teampulsehead { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
963
966
  .at-teampulsetitle { font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-primary); }
@@ -1534,7 +1537,7 @@ window.__ModuleLoader__.load({
1534
1537
  h('span', { className: 'at-prowtitle' }, project.name));
1535
1538
  }
1536
1539
 
1537
- function AgentRow({ t, agent, onAgent, onAgentMore }) {
1540
+ function AgentRow({ t, agent, onAgent, onAgentMore, onAgentChat }) {
1538
1541
  const statusText = t(AGENT_STATUS[agent.status] ?? 'agentIdle');
1539
1542
  const config = agent.config ?? null;
1540
1543
  const model = config?.model ? config.model : t('agentDefaultModel');
@@ -1557,6 +1560,13 @@ window.__ModuleLoader__.load({
1557
1560
  ),
1558
1561
  ),
1559
1562
  h('span', { className: 'at-sr' }, statusText),
1563
+ h('button', {
1564
+ className: 'at-agentchat',
1565
+ type: 'button',
1566
+ 'aria-label': tf(t, 'agentOpen', { name: agent.name }),
1567
+ title: t('agentChat'),
1568
+ onClick: () => onAgentChat(agent),
1569
+ }, h(Ico, { name: 'reply', size: 13 })),
1560
1570
  h('button', {
1561
1571
  className: 'at-agentmore',
1562
1572
  type: 'button',
@@ -1569,7 +1579,8 @@ window.__ModuleLoader__.load({
1569
1579
  function TeamPulse({ t, agents, tasks, onOpen }) {
1570
1580
  const active = tasks.filter((task) => ['pending', 'in_progress', 'pending_acceptance'].includes(task.status)).slice(-5);
1571
1581
  const running = agents.filter((agent) => agent.status === 'running');
1572
- if (active.length === 0 && running.length === 0) return null;
1582
+ const starting = active.length === 0 && tasks.length === 0 && running.length > 0;
1583
+ if (active.length === 0 && !starting) return null;
1573
1584
  return h('section', { className: 'at-teampulse', 'aria-label': t('teamPulseTitle') },
1574
1585
  h('div', { className: 'at-teampulsehead' },
1575
1586
  h('span', { className: 'at-teampulsetitle' }, t('teamPulseTitle')),
@@ -1616,7 +1627,7 @@ window.__ModuleLoader__.load({
1616
1627
  : h(EmptyState, { t, title: t('memberNoTasks'), body: t('memberNoTasksBody'), compact: true })));
1617
1628
  }
1618
1629
 
1619
- function LeftPanel({ t, data, projectId, scope, tab, inboxUnread, onBack, onInbox, onBoard, onProject, onNewProject, onAgent, onAgentMore, onAddAgent, onRecruit }) {
1630
+ function LeftPanel({ t, data, projectId, scope, tab, inboxUnread, onBack, onInbox, onBoard, onProject, onNewProject, onAgent, onAgentMore, onAgentChat, onAddAgent, onRecruit }) {
1620
1631
  const projects = data.projects.filter((project) => !project.archived);
1621
1632
  const activeAgents = data.agents.filter((agent) => agent.status !== 'retired');
1622
1633
  const talentAgents = data.agents.filter((agent) => agent.status === 'retired');
@@ -1666,7 +1677,7 @@ window.__ModuleLoader__.load({
1666
1677
  h('button', { className: 'at-mini', type: 'button', 'aria-label': t('addAgent'), title: t('addAgent'), onClick: onAddAgent }, h(Ico, { name: 'plus', size: 12 })),
1667
1678
  ),
1668
1679
  activeAgents.length
1669
- ? h('div', { role: 'list', 'aria-label': t('agentsAria') }, activeAgents.map((agent) => h(AgentRow, { key: agent.id, t, agent, onAgent, onAgentMore })))
1680
+ ? h('div', { role: 'list', 'aria-label': t('agentsAria') }, activeAgents.map((agent) => h(AgentRow, { key: agent.id, t, agent, onAgent, onAgentMore, onAgentChat })))
1670
1681
  : h('div', { className: 'at-colempty' }, t('emptyColumn')),
1671
1682
  talentAgents.length ? h('details', { className: 'at-archived' },
1672
1683
  h('summary', null, tf(t, 'talentPoolCount', { n: talentAgents.length })),
@@ -1780,7 +1791,7 @@ window.__ModuleLoader__.load({
1780
1791
  task.submission?.summary ? h('p', { className: 'at-donetext' }, task.submission.summary) : null);
1781
1792
  }
1782
1793
 
1783
- function ThreadPanel({ t, task, thread, project, following, onToggleFollow, onClose, converting, convert, reply, onReplyChange, onReplySend, replyBase, onContinue, onContext, note, onDismissNote, acceptChecked, onAcceptChecked, qaChecks, onQaCheck, onAccept, onChanges, actions, panelRef, replyRef }) {
1794
+ function ThreadPanel({ t, task, thread, project, following, onToggleFollow, onClose, onChat, converting, convert, reply, onReplyChange, onReplySend, replyBase, onContinue, onContext, note, onDismissNote, acceptChecked, onAcceptChecked, qaChecks, onQaCheck, onAccept, onChanges, actions, panelRef, replyRef }) {
1784
1795
  const headSeq = thread?.headSeq ?? thread?.entries?.length ?? 0;
1785
1796
  const stale = replyBase !== null && replyBase !== undefined && headSeq > replyBase;
1786
1797
  const entries = Array.isArray(thread?.entries) ? thread.entries : [];
@@ -1790,6 +1801,7 @@ window.__ModuleLoader__.load({
1790
1801
  h('div', { className: 'at-rhmeta', title: t('thread') + ' · #' + taskNo(task) }, t('thread') + ' · #' + taskNo(task)),
1791
1802
  h('div', { className: 'at-rhtitle', title: task.title }, task.title)),
1792
1803
  h(FollowToggle, { t, taskNo: taskNo(task), following, onToggle: onToggleFollow }),
1804
+ onChat ? h('button', { className: 'at-rbtn', type: 'button', onClick: onChat }, t('threadChat')) : null,
1793
1805
  h('button', { className: 'at-rbtn', type: 'button', 'aria-expanded': Boolean(converting), onClick: convert.onOpen }, t('convert')),
1794
1806
  h('button', { className: 'at-rbtn at-iconbtn', type: 'button', 'aria-label': t('closeThread'), title: t('closeThread'), onClick: onClose }, h(Ico, { name: 'close', size: 14 }))),
1795
1807
  note ? h('div', { className: 'at-rnote', role: 'status' },
@@ -2536,6 +2548,15 @@ window.__ModuleLoader__.load({
2536
2548
  openProfile(agent);
2537
2549
  }
2538
2550
  }
2551
+ function openTaskAgentChat(task) {
2552
+ const memberId = task?.assigneeId ?? task?.ownerId;
2553
+ const agent = data.agents.find((candidate) => candidate.id === memberId) ?? null;
2554
+ if (agent === null) {
2555
+ say(t('agentChatUnavailable'));
2556
+ return;
2557
+ }
2558
+ void openAgentChat(agent);
2559
+ }
2539
2560
  function openProfile(agent) {
2540
2561
  setProfileAgent(agent);
2541
2562
  setPName(agent.name ?? '');
@@ -2904,6 +2925,7 @@ window.__ModuleLoader__.load({
2904
2925
  onNewProject: () => { closeMobile(); openNewProject(); },
2905
2926
  onAgent: (agent) => { closeMobile(); openMemberView(agent); },
2906
2927
  onAgentMore: (agent) => { closeMobile(); openProfile(agent); },
2928
+ onAgentChat: (agent) => { closeMobile(); void openAgentChat(agent); },
2907
2929
  onAddAgent: () => { closeMobile(); openAddAgent(); },
2908
2930
  onRecruit: (agent) => { closeMobile(); recruit(agent); },
2909
2931
  };
@@ -3063,6 +3085,7 @@ window.__ModuleLoader__.load({
3063
3085
  following: followingOf(selectedTask),
3064
3086
  onToggleFollow: () => toggleFollow(selectedTask),
3065
3087
  onClose: closeThread,
3088
+ onChat: () => openTaskAgentChat(selectedTask),
3066
3089
  converting,
3067
3090
  convert: {
3068
3091
  onOpen: openConvert,
package/index.js CHANGED
@@ -20,6 +20,7 @@ import { classifyLlmFailure, retryDecision } from './lib/runtime/failure-policy.
20
20
  import { memberIdentity } from './lib/domain/member-profile.js';
21
21
  import { loadSkillContext } from './lib/runtime/skill-loader.js';
22
22
  import { formatSkillCatalog } from './lib/runtime/skill-catalog.js';
23
+ import { isSkillExcluded } from './lib/runtime/skill-policy.js';
23
24
  import { installFileSkillProvider } from './lib/runtime/skill-provider.js';
24
25
  import { apply as applySkillOpt } from './lib/vendor/skillopt/src/index.js';
25
26
  import { createMemberPermissionGuard } from './lib/host/permission-guard.js';
@@ -33,7 +34,7 @@ export const name = 'ai-team';
33
34
  /** The workbench only makes sense where the Web carrier exists. */
34
35
  export const inject = ['webServer', 'permissionPresets', 'sandboxPolicy'];
35
36
 
36
- const VERSION = '0.3.14';
37
+ const VERSION = '0.3.15';
37
38
  const PROJECT_ID = 'prj_main';
38
39
  const HUMAN_ID = 'mem_human';
39
40
 
@@ -126,13 +127,47 @@ export function apply(ctx) {
126
127
  }
127
128
  return null;
128
129
  };
130
+ const workspaceForProject = async (projectRuntime) => {
131
+ const registry = ctx.get('workspaceRegistry');
132
+ const path = projectRuntime?.service?.state?.projects?.[projectRuntime.id]?.workspacePath;
133
+ if (registry === undefined || typeof path !== 'string' || path === '') return null;
134
+ try {
135
+ const existing = await registry.resolveByPath?.(path);
136
+ if (existing !== undefined && existing !== null) return existing;
137
+ const created = await registry.create?.(path, projectRuntime.service.state.projects[projectRuntime.id]?.name ?? '文渊阁');
138
+ return created ?? null;
139
+ } catch (error) {
140
+ console.warn('[ai-team] workspace unavailable:', error?.message ?? error);
141
+ return null;
142
+ }
143
+ };
144
+ const attachMemberWorkspace = async (projectRuntime, sessionId) => {
145
+ if (typeof sessionId !== 'string' || sessionId === '') return;
146
+ const workspace = await workspaceForProject(projectRuntime);
147
+ if (workspace === null || workspace === undefined || typeof workspace.attachSession !== 'function') return;
148
+ try {
149
+ await workspace.attachSession(sessionId);
150
+ } catch (error) {
151
+ console.warn('[ai-team] member session workspace attach failed:', error?.message ?? error);
152
+ }
153
+ };
129
154
  const access = validatePermissionProfile({
130
155
  permissionPresets: ctx.get('permissionPresets'),
131
156
  sandboxPolicy: ctx.get('sandboxPolicy'),
132
157
  });
133
158
  const ready = access.ok
134
159
  ? boot()
135
- .then((value) => { booted = value; return value; })
160
+ .then(async (value) => {
161
+ booted = value;
162
+ for (const runtime of value.runtimes.values()) {
163
+ for (const member of Object.values(runtime.service.state.members ?? {})) {
164
+ if (member.kind === 'agent' && typeof member.sessionId === 'string') {
165
+ await attachMemberWorkspace(runtime, member.sessionId);
166
+ }
167
+ }
168
+ }
169
+ return value;
170
+ })
136
171
  .catch((error) => {
137
172
  bootError = error;
138
173
  console.error('[ai-team] ledger boot failed:', error?.message ?? error);
@@ -479,6 +514,7 @@ export function apply(ctx) {
479
514
  throw error;
480
515
  }
481
516
  activations.set(result.childId, result.activation);
517
+ await attachMemberWorkspace(projectRuntime, result.childId);
482
518
  return {
483
519
  ok: true,
484
520
  member: bound.member,
@@ -660,7 +696,7 @@ export function apply(ctx) {
660
696
  teamTool: ctx.get('tools')?.definitions?.().some((definition) => definition.name === 'team') ?? null,
661
697
  skillsService: skillsService !== null && skillsService !== undefined,
662
698
  skillNames: skillsService?.list
663
- ? await Promise.resolve(skillsService.list({})).then((rows) => rows.map((skill) => skill.name)).catch(() => [])
699
+ ? await Promise.resolve(skillsService.list({})).then((rows) => rows.map((skill) => skill.name).filter((skillName) => !isSkillExcluded(skillName))).catch(() => [])
664
700
  : [],
665
701
  skilloptTools: tools?.definitions?.().map((definition) => definition.name).filter((name) => name.startsWith('skillopt_')) ?? [],
666
702
  presets: await Promise.resolve(ctx.get('agentPresets')?.list?.()).catch(() => []),
@@ -5,6 +5,8 @@
5
5
  * demand instead of guessing.
6
6
  */
7
7
 
8
+ import { isSkillExcluded } from './skill-policy.js';
9
+
8
10
  function clean(value, limit) {
9
11
  const text = typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
10
12
  return text.length > limit ? `${text.slice(0, limit - 1)}…` : text;
@@ -16,8 +18,10 @@ export async function formatSkillCatalog(skills, options = {}) {
16
18
  ...(options.cwd ? { cwd: options.cwd } : {}),
17
19
  }).catch(() => []);
18
20
  if (!Array.isArray(summaries) || summaries.length === 0) return '';
21
+ const available = summaries.filter((skill) => !isSkillExcluded(skill?.name));
22
+ if (available.length === 0) return '';
19
23
  const limit = Number.isSafeInteger(options.limit) && options.limit > 0 ? options.limit : 12;
20
- const rows = summaries.slice(0, limit).map((skill) => {
24
+ const rows = available.slice(0, limit).map((skill) => {
21
25
  const name = clean(skill?.name, 80);
22
26
  const description = clean(skill?.description, 240);
23
27
  const when = clean(skill?.whenToUse, 180);
@@ -1,5 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { basename, join } from 'node:path';
3
+ import { isSkillExcluded } from './skill-policy.js';
3
4
 
4
5
  const MAX_SKILL_CHARS = 1800;
5
6
 
@@ -11,6 +12,7 @@ export async function loadSkillContext(member, dataDir) {
11
12
  for (const raw of skills.slice(0, 4)) {
12
13
  const name = basename(String(raw)).replace(/[^a-zA-Z0-9._-]/g, '');
13
14
  if (name === '' || name === '.' || name === '..' || name.startsWith('.')) continue;
15
+ if (isSkillExcluded(name)) continue;
14
16
  const candidates = [join(dataDir, 'skills', name, 'SKILL.md'), join(dataDir, 'skills', `${name}.md`)];
15
17
  for (const candidate of candidates) {
16
18
  try {
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Skills that must not be exposed by the AI Team plugin.
3
+ *
4
+ * These may still exist in the user's global DSH skill directory; the Team
5
+ * provider and prompt catalog filter them so they never enter a member's Team
6
+ * context or the workbench skill list.
7
+ */
8
+ export const EXCLUDED_SKILL_NAMES = Object.freeze(new Set(['huashu-nuwa']));
9
+
10
+ export function isSkillExcluded(name) {
11
+ return typeof name === 'string' && EXCLUDED_SKILL_NAMES.has(name);
12
+ }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
9
9
  import { join } from 'node:path';
10
+ import { isSkillExcluded } from './skill-policy.js';
10
11
 
11
12
  const MAX_BODY_BYTES = 512 * 1024;
12
13
 
@@ -61,6 +62,7 @@ function scanRoot(root, source) {
61
62
  const fields = parseFrontmatter(text);
62
63
  const name = unquote(fields.name) || entry.name;
63
64
  if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) continue;
65
+ if (isSkillExcluded(name)) continue;
64
66
  candidates.push({
65
67
  name,
66
68
  description: unquote(fields.description) || `Skill ${name}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suifei/dsh-ai-team",
3
- "version": "0.3.14",
3
+ "version": "0.3.15",
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",