dsh-tabbit 0.3.0 → 0.3.2

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,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.2
4
+
5
+ - `tabbit_browser` gains `list_tasks: true`: a zero-side-effect readback of
6
+ the browser tasks this session already has open (which one an omitted
7
+ `task` would target next), read from the in-memory session registry only.
8
+ A memory jog for when the model has lost track after a gap in the
9
+ conversation, instead of guessing a task name or opening a redundant one.
10
+ - `claim_tabs` now works when the task already exists, not only when the
11
+ same call creates it. Runtime Service silently drops a creation-time
12
+ `--claim-tab` on a reused task, so previously the plugin threw
13
+ `CLAIM_REQUIRES_NEW_TASK` and forced starting a fresh task just to attach
14
+ a tab the user pointed at mid-conversation. It now falls back to the
15
+ standalone `claim` CLI command for tasks the session already knows about,
16
+ so an already-open tab can join ongoing browser state instead of always
17
+ starting over.
18
+ - Fixed the bundled `SKILL.md`'s frontmatter parser assuming LF-only line
19
+ endings: a copy checked out or published with CRLF endings would have had
20
+ its raw YAML header served to the model as skill content instead of being
21
+ stripped.
22
+
23
+ ## 0.3.1
24
+
25
+ - Fixed the web client plugin never activating on current DSH hosts — the web
26
+ UI showed `dsh-tabbit: pending (waiting for service: conversationEvents)`.
27
+ The DSH client runtime refactor (already part of `0.1.2-alpha.1`) renamed the
28
+ `conversationEvents` service to `uiConversation` and moved node registration
29
+ from `service.register()` to `service.events.register()`. The client plugin
30
+ now injects `uiConversation`; the `/tabbit-info` status card, the `@tab`
31
+ composer source, and the watching-instance hint all had been blocked behind
32
+ that pending inject and work again. The `conversation.chat.node` keyed slot
33
+ registration and the Definition contract (`match`/`start`/`update`/
34
+ `buildViewNode`) are unchanged.
35
+
3
36
  ## 0.3.0
4
37
 
5
38
  - Major upgrade: browser automation now runs through the native
package/README.en.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # dsh-tabbit — Tabbit's Official DeepSeek Harness Plugin
2
2
 
3
- **English** | [简体中文](README.md)
3
+ **English** | [简体中文](README.md) | [Changelog](CHANGELOG.md)
4
4
 
5
- ![Tabbit Browser for DeepSeek Harness](tabbit-for-dsh.png?v=2)
5
+ ![Tabbit Browser for DeepSeek Harness](assets/dsh-tabbit-banner.png)
6
6
 
7
7
  A DeepSeek Harness (dsh) plugin bundle for Tabbit Browser. Through this
8
8
  plugin, dsh calls on Tabbit to complete agent tasks: real pages, real login
@@ -48,6 +48,12 @@ dsh plugin --profile web add link:/path/to/dsh-tabbit # local development
48
48
  > upgrade notice from the daily update check, and simply re-running the
49
49
  > install command upgrades in place.
50
50
 
51
+ ## Community & Support
52
+
53
+ Scan the QR code below to join the **dsh-tabbit Developer Group** to share feedback, ask questions, and discuss new features:
54
+
55
+ ![dsh-tabbit Developer Group](assets/dsh-tabbit-developer-community-qr.png.jpg)
56
+
51
57
  ## Settings
52
58
 
53
59
  ### Basic configuration
@@ -103,6 +109,26 @@ gets denied — that combination gets a denial message pointing at the
103
109
 
104
110
  ## Development
105
111
 
112
+ Developing and testing this project depends on a local
113
+ [deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) checkout.
114
+ The `@deepseek-ai/*` packages published on npm generally lag too far behind
115
+ to install directly as dependencies, so you need to build a harness checkout
116
+ locally first, then point the repo's `.dsh-harness` symlink at it with the
117
+ script below (that path is already ignored in `.gitignore`). That's the only
118
+ place you configure it — neither `tsconfig.json` nor `package.json` needs
119
+ any changes.
120
+
121
+ Get and build deepseek-harness (skip if you already have one):
122
+ ```bash
123
+ git clone https://github.com/deepseek-ai/deepseek-harness && cd deepseek-harness && pnpm install && pnpm build && cd ..
124
+ ```
125
+
126
+ Point this repo at it — replace `/path/to/deepseek-harness` with your actual checkout path:
127
+ ```bash
128
+ npm run link-harness -- /path/to/deepseek-harness # or set DSH_HARNESS_PATH
129
+ ```
130
+
131
+ Install and build:
106
132
  ```bash
107
133
  pnpm install && pnpm build # tsc → lib/
108
134
  npm test # build + node --test tests/
@@ -111,6 +137,5 @@ npm test # build + node --test tests/
111
137
  ## Known limitations / Roadmap
112
138
 
113
139
  - Mentioning bookmarks/favorites isn't supported yet.
114
- - `claim_tabs` isn't usable end-to-end.
115
140
  - Screenshots entering context require a model route that accepts image input.
116
141
  - Windows regression testing is limited.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # dsh-tabbit — Tabbit 的 DeepSeek Harness 官方插件
2
2
 
3
- [English](README.en.md) | **简体中文**
3
+ [English](README.en.md) | **简体中文** | [Changelog](CHANGELOG.md)
4
4
 
5
- ![Tabbit Browser for DeepSeek Harness](tabbit-for-dsh.png?v=2)
5
+ ![Tabbit Browser for DeepSeek Harness](assets/dsh-tabbit-banner.png)
6
6
 
7
7
  Tabbit Browser 的 DeepSeek Harness(dsh)插件包(bundle)。dsh 可以通过此插件调用 Tabbit 完成 Agent 任务:真实页面、真实登录态、真实交互,经原生 code-first 工具驱动(不走 shell 转发)。适用于网页自动化、信息提取、QA 与评测。
8
8
 
@@ -41,6 +41,12 @@ dsh plugin --profile web add link:/path/to/dsh-tabbit # 本地开发
41
41
 
42
42
  > 本包取代早期的 `tabbit-browser` skill-only 插件;npm 上的 0.2.x 版本也由本版本接续——0.2.x 用户经每日更新检查会收到升级提示,重跑安装命令即可原地升级。
43
43
 
44
+ ## 社区与交流
45
+
46
+ 欢迎扫描下方二维码加入 **dsh-tabbit 开发者交流群**,交流使用心得、反馈问题与探讨新特性:
47
+
48
+ ![dsh-tabbit 开发者交流群](assets/dsh-tabbit-developer-community-qr.png.jpg)
49
+
44
50
  ## 设置
45
51
 
46
52
  ### 基本配置
@@ -74,6 +80,21 @@ Full access 说明:dsh 的 `danger-full-access` 权限预设会给会话写入
74
80
 
75
81
  ## 开发
76
82
 
83
+ 项目的开发和测试依赖本地的 [deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) 环境。
84
+ 由于 `@deepseek-ai/*` 系列包在 npm 上的发布版本普遍滞后,不能直接作为依赖安装,因此需要先在本地构建好一份 harness 检出,再通过下方脚本将仓库内的 .dsh-harness 符号链接指向它(该路径已在 .gitignore 中忽略)。
85
+ 整个流程只需配置这一处,`tsconfig.json` 和 `package.json` 均无需改动。
86
+
87
+ Deepseek Harness 代码获取及安装(如果已安装请忽略)
88
+ ```bash
89
+ git clone https://github.com/deepseek-ai/deepseek-harness && cd deepseek-harness && pnpm install && pnpm build && cd ..
90
+ ```
91
+
92
+ 将本机的 Deepseek 环境路径修改并 ln 到本项目,将 `/path/to/deepseek-harness` 改为你的实际目录。
93
+ ```bash
94
+ npm run link-harness -- /path/to/deepseek-harness # 或设置环境变量 DSH_HARNESS_PATH
95
+ ```
96
+
97
+ 安装和编译
77
98
  ```bash
78
99
  pnpm install && pnpm build # tsc → lib/
79
100
  npm test # 构建 + node --test tests/
@@ -82,6 +103,5 @@ npm test # 构建 + node --test tests/
82
103
  ## 已知限制 / 路线图
83
104
 
84
105
  - 收藏夹/书签的提及暂不支持。
85
- - `claim_tabs` 端到端暂不可用。
86
106
  - 截图进上下文要求当前模型路由支持图像输入。
87
107
  - Windows 版本回归测试有限。
package/client/client.js CHANGED
@@ -35,8 +35,9 @@ var module = { exports: {} }; var exports = module.exports;
35
35
  * dsh 客户端插件协议与服务端 Cordis 如出一辙:导出 {name, inject, apply(ctx)};
36
36
  * 这里注入的 'inputTriggers' 是 dsh-client-ui-input-trigger 提供的"输入框
37
37
  * 触发菜单"服务(package.json dsh.client.inject 里声明了要它一起装);
38
- * 'conversationEvents'/'slots' 是 dsh-client-runtime(web 包核心)恒定
39
- * 提供的服务,不用额外声明。
38
+ * 'uiConversation'/'slots' 是 web 客户端核心恒定提供的服务,不用额外声明
39
+ * ('uiConversation' 是宿主 0.1.2-alpha.1 客户端重构后 'conversationEvents'
40
+ * 的新名字,注册入口相应从 service.register() 移到了 service.events.register())。
40
41
  * 共享的触发菜单负责渲染候选列表 UI——@tab 部分只提供数据,不需要 React。
41
42
  *
42
43
  * 功能三:把服务端 /tabbit-info 命令落下的 tabbit/status 会话事件折成
@@ -310,8 +311,8 @@ function TabbitStatusNodeView({ node }) {
310
311
  }
311
312
 
312
313
  /*
313
- * tabbit/status 事件 → 聊天节点的 Definition(conversationEvents 服务的
314
- * 契约形状,同 dsh 文档 adding-a-conversation-node 的单事件业务写法)。
314
+ * tabbit/status 事件 → 聊天节点的 Definition(uiConversation 服务的契约
315
+ * 形状,同 dsh 文档 adding-a-conversation-node 的单事件业务写法)。
315
316
  * 每次执行落一条整值事件,以 event.seq 为节点身份各自独立成行——重复
316
317
  * 执行 /tabbit-info 天然多行并存、不互相覆盖;anchorSeq 用事件自身的
317
318
  * seq,位置正好落在命令行节点(command/run)之后。start/update 都是纯
@@ -345,7 +346,7 @@ const tabbitStatusNode = {
345
346
 
346
347
  module.exports = {
347
348
  name: 'dsh-tabbit-client',
348
- inject: ['inputTriggers', 'conversationEvents', 'slots'],
349
+ inject: ['inputTriggers', 'uiConversation', 'slots'],
349
350
  apply(ctx) {
350
351
  // 观看实例打点:告诉服务端"是哪个浏览器在看这个页面"。fire-and-forget
351
352
  // (不 await、双层吞错)——纯属锦上添花的提示,任何失败都不能影响页面。
@@ -370,14 +371,14 @@ module.exports = {
370
371
  }
371
372
  }
372
373
 
373
- // 功能三:/tabbit-info 状态卡。conversationEvents/slots 已在 inject 里
374
+ // 功能三:/tabbit-info 状态卡。uiConversation/slots 已在 inject 里
374
375
  // 声明(apply 只会在它们就绪后运行),这里仍按本文件惯例做形状防御。
375
376
  // 两者内部都是 ctx.effect 式注册(随插件 fiber 卸载自动回收),直接调。
376
- const conversationEvents = ctx.get('conversationEvents');
377
+ const uiConversation = ctx.get('uiConversation');
377
378
  const slots = ctx.get('slots');
378
- if (conversationEvents && typeof conversationEvents.register === 'function'
379
+ if (uiConversation && typeof uiConversation.events?.register === 'function'
379
380
  && slots && typeof slots.inject === 'function') {
380
- conversationEvents.register(tabbitStatusNode);
381
+ uiConversation.events.register(tabbitStatusNode);
381
382
  slots.inject('conversation.chat.node', () => slots.register(
382
383
  { name: 'conversation.chat.node', key: 'tabbit-status' },
383
384
  TabbitStatusNodeView,
package/lib/core/index.js CHANGED
@@ -284,6 +284,14 @@ export class TabbitService {
284
284
  this.defaultTaskNames.set(agentId, name);
285
285
  return name;
286
286
  }
287
+ /*
288
+ * 只读查询当前已定型的默认任务名——不存在就返回 undefined,绝不像
289
+ * defaultTaskFor 那样顺手创建并定型一个新名字(给 list_tasks 之类的查询
290
+ * 路径用:查询本身不该有"顺带开一个任务"的副作用)。
291
+ */
292
+ currentDefaultTask(agentId) {
293
+ return this.defaultTaskNames.get(agentId);
294
+ }
287
295
  /* 登记共享 fetch 任务在某实例上被用过(供 releaseAll 精准清理)。 */
288
296
  markFetchTaskUsed(instanceId) {
289
297
  this.fetchTaskInstances.add(instanceId);
@@ -455,15 +463,21 @@ const SKILL_FALLBACK_DESCRIPTION = "Recipes for operating the user's Tabbit Brow
455
463
  * (正文才是要进模型上下文的内容)。
456
464
  * 只认我们自己写的这一层【扁平 key: value】(值可带引号),认不出的行直接
457
465
  * 跳过——不为一个几行的 frontmatter 引入 YAML 依赖,也不假装能解析嵌套结构。
466
+ * 开合分隔符按开头是 \n 还是 \r\n 定档、全程沿用同一种换行符(含内部按行
467
+ * split 取字段)——容忍 Windows checkout(git autocrlf)产出的 CRLF 文件,
468
+ * 不强行改写正文本身的换行风格。
458
469
  */
459
- function parseSkillDocument(source) {
460
- if (!source.startsWith('---\n'))
470
+ export function parseSkillDocument(source) {
471
+ const newline = source.startsWith('---\r\n') ? '\r\n' : '\n';
472
+ const opening = `---${newline}`;
473
+ if (!source.startsWith(opening))
461
474
  return { fields: {}, body: source };
462
- const end = source.indexOf('\n---\n', 4);
475
+ const closing = `${newline}---${newline}`;
476
+ const end = source.indexOf(closing, opening.length);
463
477
  if (end === -1)
464
478
  return { fields: {}, body: source };
465
479
  const fields = {};
466
- for (const line of source.slice(4, end).split('\n')) {
480
+ for (const line of source.slice(opening.length, end).split(newline)) {
467
481
  const match = /^([A-Za-z][\w-]*):\s*(.*)$/u.exec(line);
468
482
  if (match === null)
469
483
  continue;
@@ -472,7 +486,7 @@ function parseSkillDocument(source) {
472
486
  if (value !== '')
473
487
  fields[match[1]] = value;
474
488
  }
475
- return { fields, body: source.slice(end + 5) };
489
+ return { fields, body: source.slice(end + closing.length) };
476
490
  }
477
491
  /*
478
492
  * 读盘 + 解析,组装 dsh 要的两样东西:候选卡片(list 用)与正文(get 用)。
@@ -23,12 +23,15 @@
23
23
  * - 任务【只能看到自己打开的或被显式 claim(认领)的标签页】,无法枚举
24
24
  * 用户的其它标签页——这是浏览器侧的安全边界;
25
25
  * - 整机最多 8 个并发任务;单次求值最长 120 秒;
26
- * - 本客户端用到的 CLI 动词:`nodejs`(求值)、`finish`(结束任务,
27
- * keep 语义两代有别,见 finishTask 注释)、`receipt`(查回执)、
28
- * `checkpoint`(检查点)、`resource`(分块读资源)、`tasks`(列任务)。
29
- * 新代 CLI(1.11.16+)另有 tabs/claim/resume/screenshot/inspect/paste
30
- * persistent 持久模式(JSON 帧协议)——本客户端尚未使用,见浏览器
31
- * 共享 skill(~/.agents/skills/tabbit)与 TabbitDance 源码。
26
+ * - 本客户端用到的 CLI 动词:`nodejs`(求值,创建时可带 --claim-tab)、
27
+ * `finish`(结束任务,keep 语义两代有别,见 finishTask 注释)、
28
+ * `receipt`(查回执)、`checkpoint`(检查点)、`resource`(分块读资源)、
29
+ * `tasks`(列任务)、`claim`(对已存在的任务追加认领标签页,见
30
+ * claimTabs()——真机 `--help` 确认过是独立顶层子命令,不是只存在于
31
+ * persistent 帧协议里)。新代 CLI(1.11.16+)另有 tabs/resume/
32
+ * screenshot/inspect/paste 和 persistent 持久模式(JSON 帧协议)——
33
+ * 本客户端尚未使用,见浏览器共享 skill(~/.agents/skills/tabbit)与
34
+ * TabbitDance 源码。
32
35
  *
33
36
  * ─── 一次 evaluate 的完整旅程 ─────────────────────────────────────────
34
37
  *
@@ -371,6 +374,26 @@ export class TabbitClient {
371
374
  async checkpoint(task) {
372
375
  return await this.invoke(['checkpoint', '--task', task], '', CONTROL_TIMEOUT_MS);
373
376
  }
377
+ /*
378
+ * 把一个或多个可用(available)标签页追加认领进一个【已经存在】的任务
379
+ * (`claim` 命令)。与 evaluate() 的 claimTabs 只在任务创建那一刻生效不同,
380
+ * 这是独立的一次性 CLI 子命令——真机 `tabbit-cli --help` 的 usage 行确认过
381
+ * 它是顶层动词(`claim --task <name> --tab <id>...`),不是只存在于 persistent
382
+ * 帧协议里;真机验证过对一个此前已创建好的任务追加 claim 可行。
383
+ * 批量原子:文档明确"duplicate, stale, busy, unsupported, or cross-window
384
+ * inputs do not partially claim"——要么全部认领成功,要么整批失败并抛错,
385
+ * 调用方不需要处理"部分成功"的中间态。
386
+ */
387
+ async claimTabs(task, tabIds) {
388
+ const argv = ['claim', '--task', task];
389
+ for (const tab of tabIds)
390
+ argv.push('--tab', String(tab));
391
+ const response = (await this.invoke(argv, '', CONTROL_TIMEOUT_MS));
392
+ return {
393
+ ...(typeof response.groupId === 'string' ? { groupId: response.groupId } : {}),
394
+ ...(typeof response.ownedPageCount === 'number' ? { ownedPageCount: response.ownedPageCount } : {}),
395
+ };
396
+ }
374
397
  /* 列出当前实例上的所有任务(`tasks` 命令)。返回值形状异常时兜底为空数组。 */
375
398
  async listTasks() {
376
399
  const value = await this.invoke(['tasks'], '', CONTROL_TIMEOUT_MS);
@@ -56,7 +56,8 @@ const DESCRIPTION = [
56
56
  '`globalThis` persists across calls in the same task — store state instead of re-scraping. `console.*` output is discarded; communicate via the return value only.',
57
57
  'Return small JSON-serializable values. To show yourself a screenshot, save it with `page.screenshot({ path: artifactPath("x.png") })` and return `{ screenshots: [artifactPath("x.png")] }`.',
58
58
  'Pass `finish: true` on your last call once this piece of browser work is done, so the task and its tab group close instead of sitting open indefinitely; add `keep_tabs: true` alongside it if the tabs should stay open regardless (e.g. tabs the user pointed you at).',
59
- "Pass `list_tabs: true` (no code needed) to list every tab currently open in the user's browser — all windows, including tabs you did not open; metadata only, zero side effects. Use it to find a tab the user referred to, then attach that tab via `claim_tabs` when creating a new task.",
59
+ "Pass `list_tabs: true` (no code needed) to list every tab currently open in the user's browser — all windows, including tabs you did not open; metadata only, zero side effects. Use it to find a tab the user referred to, then attach that tab via `claim_tabs` (works whether the task is new or already exists).",
60
+ 'Pass `list_tasks: true` (no code needed) to list the browser tasks this session already has open — a memory jog for when you have lost track after a gap in the conversation, before deciding whether to reuse one or start fresh.',
60
61
  'Call `tabbit_browser_install` once before your first browser call in a session, and load the `tabbit` skill for recipes before non-trivial work.',
61
62
  ].join(' ');
62
63
  /* 渲染进上下文的文本上限(30k 字符),防止巨型结果撑爆模型上下文。 */
@@ -94,7 +95,7 @@ export function apply(ctx) {
94
95
  // ctx.tabbit.listAllTabs() 而非求值——所以离线时如实报离线。
95
96
  list_tabs: {
96
97
  type: 'boolean',
97
- description: "List every tab of the user's running browser instead of running code: returns { tabCount, tabs: [{ tabId, windowId, index, title, url, active, state, group? }], ... }. state 'available' means the tab can be attached via claim_tabs (new task only); 'busy' means another automation task owns it. tabCount is always the full total; at most 100 entries are returned (listTruncated: true when clipped) — narrow with tabs_filter instead of re-listing. Zero side effects; the browser is never launched for a listing. When set, code/task/finish are ignored.",
98
+ description: "List every tab of the user's running browser instead of running code: returns { tabCount, tabs: [{ tabId, windowId, index, title, url, active, state, group? }], ... }. state 'available' means the tab can be attached via claim_tabs (new task only); 'busy' means another automation task owns it. tabCount is always the full total; at most 100 entries are returned (listTruncated: true when clipped) — narrow with tabs_filter instead of re-listing. Zero side effects; the browser is never launched for a listing. Takes precedence over list_tasks if both are set. When set, code/task/finish are ignored.",
98
99
  },
99
100
  // tabs_filter:大浏览器(真机 170+ 标签页)上全量清单会顶到渲染截断线,
100
101
  // 模型必须能按关键词收窄而不是反复重列。
@@ -102,6 +103,14 @@ export function apply(ctx) {
102
103
  type: 'string',
103
104
  description: 'With list_tabs: case-insensitive substring matched against tab title and url, to find a specific tab in a large browser (e.g. the site name the user mentioned).',
104
105
  },
106
+ // list_tasks:查这个会话自己已经开过哪些任务——纯内存读取会话任务
107
+ // 登记表(sessionTaskRegistry/defaultTaskNames),不碰 CLI 也不碰
108
+ // 浏览器。模型隔了几轮容易忘记自己还有任务开着,给它一个零成本的
109
+ // 记忆点,别每次都靠猜测复用任务名或者重开一个。
110
+ list_tasks: {
111
+ type: 'boolean',
112
+ description: "List the browser tasks this session currently has open, without touching the browser: returns { taskCount, defaultTask?, tasks: [{ task, isDefault }] }. defaultTask is the task an omitted `task` argument would target next. Use this when you've lost track of what you left open earlier in the conversation. Ignored if list_tabs is also set. When set, code/task/finish are ignored.",
113
+ },
105
114
  // task:显式指定任务名(不同名字 = 完全独立的浏览器状态)。
106
115
  // 一般不传,用会话默认任务即可。
107
116
  task: {
@@ -125,15 +134,18 @@ export function apply(ctx) {
125
134
  type: 'integer',
126
135
  description: 'Per-call evaluation timeout in milliseconds, max 120000 (default 120000).',
127
136
  },
128
- // claim_tabs:把用户【明确指给模型】的已有标签页认领进任务。
129
- // Runtime Service 的规则是"只在任务创建那一刻生效",复用任务时它把
130
- // claim【静默忽略】——静默成功最坑,所以 client.ts 的 evaluateOnce 看到
131
- // 回执里 reused=true 就自己抛 CLAIM_REQUIRES_NEW_TASK(报错的是我们,
132
- // 不是服务端),让模型知道要换个新任务名。
137
+ // claim_tabs:把用户【明确指给模型】的已有标签页认领进任务。两条路径:
138
+ // - 任务是本次调用【新建】的:走 evaluate() 自带的 --claim-tab(创建时
139
+ // 生效);
140
+ // - 任务【已经存在】(本会话此前用过,见 sessionTasks 判定):改走
141
+ // 独立的 claim 一次性子命令(client.ts 的 claimTabs())——真机
142
+ // 确认过这是顶层命令,不是只在任务创建那一刻才生效。
143
+ // 模型不需要关心走的是哪条路径,两边结果都会体现在返回值里
144
+ // (claimedTabCount/ownedPageCount 或 claimError)。
133
145
  claim_tabs: {
134
146
  type: 'array',
135
147
  items: { type: 'integer' },
136
- description: 'Tab ids EXPLICITLY provided by the user to attach their existing tabs to this task. Only honored when the task is first created.',
148
+ description: 'Tab ids EXPLICITLY provided by the user to attach their existing tabs to this task works whether this call creates the task or the task already exists.',
137
149
  },
138
150
  // finish:本次调用后关闭任务(从任务列表消失;除非 keep_tabs,标签组
139
151
  // 也一起关)。让模型按任务性质自己判断:一次性查询干完就关;用户可能
@@ -160,6 +172,9 @@ export function apply(ctx) {
160
172
  timeoutMs: 160_000,
161
173
  async execute(args, exec) {
162
174
  const tabbit = ctx.tabbit;
175
+ // 极端情况下工具可能无所属 agent(如某些编排形态),用 'shared' 兜底。
176
+ // list_tasks 分支也要用它,故提到最前面统一算一次。
177
+ const agentId = exec.agent !== undefined ? String(exec.agent.id) : 'shared';
163
178
  // list_tabs 分支:直连清单,与求值路径完全无关(不碰任务/agent/CLI)。
164
179
  if (args.list_tabs === true) {
165
180
  try {
@@ -202,19 +217,53 @@ export function apply(ctx) {
202
217
  throw error;
203
218
  }
204
219
  }
220
+ // list_tasks 分支:会话任务登记表快照,纯内存读取(sessionTasks/
221
+ // currentDefaultTask 都不做 I/O),比 list_tabs 还更轻——连 Runtime
222
+ // Service 的被动端点都不连。
223
+ if (args.list_tasks === true) {
224
+ const tasks = tabbit.sessionTasks(agentId);
225
+ const defaultTask = tabbit.currentDefaultTask(agentId);
226
+ return {
227
+ status: 'succeeded',
228
+ taskCount: tasks.length,
229
+ ...(defaultTask !== undefined ? { defaultTask } : {}),
230
+ tasks: tasks.map((task) => ({ task, isDefault: task === defaultTask })),
231
+ };
232
+ }
205
233
  // 常规求值必须有代码(schema 层已放开 required,这里补明确报错)。
206
234
  if (args.code === undefined || args.code === '') {
207
235
  return {
208
236
  status: 'failed',
209
- error: 'code is required unless list_tabs is true',
237
+ error: 'code is required unless list_tabs or list_tasks is true',
210
238
  errorCode: 'MISSING_CODE',
211
239
  };
212
240
  }
213
- // 极端情况下工具可能无所属 agent(如某些编排形态),用 'shared' 兜底。
214
- const agentId = exec.agent !== undefined ? String(exec.agent.id) : 'shared';
215
241
  // 任务名决策:显式 task 优先,否则取会话默认任务(首调可被 label 定名)。
216
242
  const task = args.task !== undefined && args.task !== '' ? args.task : tabbit.defaultTaskFor(agentId, args.label);
217
243
  const client = tabbit.client();
244
+ const requestedClaims = args.claim_tabs !== undefined && args.claim_tabs.length > 0 ? args.claim_tabs : undefined;
245
+ // 本会话登记表里已经有这个任务名 → 一定是复用,创建时的 --claim-tab
246
+ // 语义上不会生效(evaluate 会撞见 reused=true 直接抛错)。改走独立的
247
+ // claim 子命令,成功/失败都不让整个调用失败,只把结果软性附加到
248
+ // 最终返回值——求值本身该跑还是照跑(模型可能只是顺手多认领一个
249
+ // 标签页,不代表这次调用的主要目的就是认领)。
250
+ let claimOutcome = {};
251
+ let claimAtCreation = requestedClaims;
252
+ if (requestedClaims !== undefined && tabbit.sessionTasks(agentId).includes(task)) {
253
+ claimAtCreation = undefined; // 已经在这里处理了,别再让 evaluate 撞 CLAIM_REQUIRES_NEW_TASK
254
+ try {
255
+ const claimed = await client.claimTabs(task, requestedClaims);
256
+ claimOutcome = {
257
+ claimedTabCount: requestedClaims.length,
258
+ ...(claimed.ownedPageCount !== undefined ? { ownedPageCount: claimed.ownedPageCount } : {}),
259
+ };
260
+ }
261
+ catch (error) {
262
+ claimOutcome = {
263
+ claimError: error instanceof TabbitCliError ? friendlyCliError(error) : String(error?.message ?? error),
264
+ };
265
+ }
266
+ }
218
267
  let outcome;
219
268
  try {
220
269
  outcome = await client.evaluate({
@@ -222,7 +271,7 @@ export function apply(ctx) {
222
271
  code: args.code,
223
272
  ...(args.read_only === true ? { readOnly: true } : {}),
224
273
  ...(args.timeout_ms !== undefined ? { timeoutMs: args.timeout_ms } : {}),
225
- ...(args.claim_tabs !== undefined && args.claim_tabs.length > 0 ? { claimTabs: args.claim_tabs } : {}),
274
+ ...(claimAtCreation !== undefined ? { claimTabs: claimAtCreation } : {}),
226
275
  signal: exec.signal, // 用户取消 → 一路传到 CLI 子进程被杀
227
276
  });
228
277
  }
@@ -268,6 +317,7 @@ export function apply(ctx) {
268
317
  error: outcome.errorMessage ?? 'evaluation failed',
269
318
  ...(outcome.taskWasReset ? { taskWasReset: true } : {}),
270
319
  ...(outcome.notes.length > 0 ? { notes: outcome.notes } : {}),
320
+ ...claimOutcome,
271
321
  ...finishOutcome,
272
322
  };
273
323
  }
@@ -281,6 +331,7 @@ export function apply(ctx) {
281
331
  task,
282
332
  ...(outcome.taskWasReset ? { taskWasReset: true } : {}),
283
333
  ...(outcome.notes.length > 0 ? { notes: outcome.notes } : {}),
334
+ ...claimOutcome,
284
335
  ...finishOutcome,
285
336
  };
286
337
  if (decoded === undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-tabbit",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Tabbit Browser bundle for DeepSeek Harness: a code-first tabbit_browser tool, browser-backed web_fetch, @tab composer mentions, a dedicated page-access permission, an environment preflight with background installer download, and a daily plugin update check.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -44,7 +44,8 @@
44
44
  "scripts": {
45
45
  "build": "tsc -p tsconfig.json",
46
46
  "prepack": "npm run build",
47
- "test": "npm run build && node --test"
47
+ "test": "npm run build && node --test",
48
+ "link-harness": "node scripts/link-harness.mjs"
48
49
  },
49
50
  "engines": {
50
51
  "node": "^22.19.0 || >=24.0.0"
@@ -62,15 +63,16 @@
62
63
  "@deepseek-ai/dsh-system-prompt": ">=0.1.1-rc.2 || >=0.1.2-alpha.1",
63
64
  "@deepseek-ai/dsh-tools": ">=0.1.1-rc.2 || >=0.1.2-alpha.1",
64
65
  "@deepseek-ai/dsh-user-approval": ">=0.1.1-rc.2 || >=0.1.2-alpha.1",
66
+ "@deepseek-ai/dsh-util-values": ">=0.1.1-rc.2 || >=0.1.2-alpha.1",
65
67
  "@deepseek-ai/dsh-web": ">=0.1.1-rc.2 || >=0.1.2-alpha.1",
66
68
  "@deepseek-ai/schemastery": ">=3.18.0"
67
69
  },
68
70
  "devDependencies": {
69
- "@deepseek-ai/dsh-llm": "0.1.1-rc.2",
70
- "@deepseek-ai/dsh-session": "0.1.1-rc.2",
71
- "@deepseek-ai/dsh-tools": "0.1.1-rc.2",
72
- "@deepseek-ai/dsh-web": "0.1.2-alpha.2",
73
- "@deepseek-ai/schemastery": "link:../deepseek-harness/vendor/schemastery",
71
+ "@deepseek-ai/dsh-llm": "link:./.dsh-harness/packages/llm/llm",
72
+ "@deepseek-ai/dsh-session": "link:./.dsh-harness/packages/core/session",
73
+ "@deepseek-ai/dsh-tools": "link:./.dsh-harness/packages/core/tools",
74
+ "@deepseek-ai/dsh-web": "link:./.dsh-harness/packages/web/web",
75
+ "@deepseek-ai/schemastery": "link:./.dsh-harness/vendor/schemastery",
74
76
  "@types/node": "^24.0.0",
75
77
  "typescript": "^5.7.0"
76
78
  },