dsh-tabbit 0.3.0 → 0.3.3
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 +41 -0
- package/README.en.md +28 -3
- package/README.md +23 -3
- package/client/client.js +19 -11
- package/lib/core/index.js +19 -5
- package/lib/runtime/client.js +29 -6
- package/lib/tool-browser/index.js +63 -12
- package/package.json +9 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.3
|
|
4
|
+
|
|
5
|
+
- Restored web-client compatibility with DSH `0.1.1-rc.2`. The client no
|
|
6
|
+
longer hard-injects either generation of the conversation service: it uses
|
|
7
|
+
`uiConversation.events` on DSH `0.1.2-alpha.1+` and falls back to
|
|
8
|
+
`conversationEvents` on `0.1.1`, so neither host remains pending while
|
|
9
|
+
waiting for the other generation's service name.
|
|
10
|
+
|
|
11
|
+
## 0.3.2
|
|
12
|
+
|
|
13
|
+
- `tabbit_browser` gains `list_tasks: true`: a zero-side-effect readback of
|
|
14
|
+
the browser tasks this session already has open (which one an omitted
|
|
15
|
+
`task` would target next), read from the in-memory session registry only.
|
|
16
|
+
A memory jog for when the model has lost track after a gap in the
|
|
17
|
+
conversation, instead of guessing a task name or opening a redundant one.
|
|
18
|
+
- `claim_tabs` now works when the task already exists, not only when the
|
|
19
|
+
same call creates it. Runtime Service silently drops a creation-time
|
|
20
|
+
`--claim-tab` on a reused task, so previously the plugin threw
|
|
21
|
+
`CLAIM_REQUIRES_NEW_TASK` and forced starting a fresh task just to attach
|
|
22
|
+
a tab the user pointed at mid-conversation. It now falls back to the
|
|
23
|
+
standalone `claim` CLI command for tasks the session already knows about,
|
|
24
|
+
so an already-open tab can join ongoing browser state instead of always
|
|
25
|
+
starting over.
|
|
26
|
+
- Fixed the bundled `SKILL.md`'s frontmatter parser assuming LF-only line
|
|
27
|
+
endings: a copy checked out or published with CRLF endings would have had
|
|
28
|
+
its raw YAML header served to the model as skill content instead of being
|
|
29
|
+
stripped.
|
|
30
|
+
|
|
31
|
+
## 0.3.1
|
|
32
|
+
|
|
33
|
+
- Fixed the web client plugin never activating on current DSH hosts — the web
|
|
34
|
+
UI showed `dsh-tabbit: pending (waiting for service: conversationEvents)`.
|
|
35
|
+
The DSH client runtime refactor (already part of `0.1.2-alpha.1`) renamed the
|
|
36
|
+
`conversationEvents` service to `uiConversation` and moved node registration
|
|
37
|
+
from `service.register()` to `service.events.register()`. The client plugin
|
|
38
|
+
now injects `uiConversation`; the `/tabbit-info` status card, the `@tab`
|
|
39
|
+
composer source, and the watching-instance hint all had been blocked behind
|
|
40
|
+
that pending inject and work again. The `conversation.chat.node` keyed slot
|
|
41
|
+
registration and the Definition contract (`match`/`start`/`update`/
|
|
42
|
+
`buildViewNode`) are unchanged.
|
|
43
|
+
|
|
3
44
|
## 0.3.0
|
|
4
45
|
|
|
5
46
|
- 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
|
-

|
|
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
|
+

|
|
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
|
-

|
|
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
|
+

|
|
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,11 @@ 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
|
-
* '
|
|
39
|
-
*
|
|
38
|
+
* 'slots' 是 web 客户端核心恒定提供的服务;会话节点注册服务在宿主
|
|
39
|
+
* 0.1.2-alpha.1 从 'conversationEvents' 政名为 'uiConversation',注册入口
|
|
40
|
+
* 也从 service.register() 移到了 service.events.register()。两者不能作为
|
|
41
|
+
* 硬注入,否则另一代宿主会让整个客户端插件永久 pending,故在 apply
|
|
42
|
+
* 中按新接口优先、旧接口兜底的顺序探测。
|
|
40
43
|
* 共享的触发菜单负责渲染候选列表 UI——@tab 部分只提供数据,不需要 React。
|
|
41
44
|
*
|
|
42
45
|
* 功能三:把服务端 /tabbit-info 命令落下的 tabbit/status 会话事件折成
|
|
@@ -310,8 +313,8 @@ function TabbitStatusNodeView({ node }) {
|
|
|
310
313
|
}
|
|
311
314
|
|
|
312
315
|
/*
|
|
313
|
-
* tabbit/status 事件 → 聊天节点的 Definition(
|
|
314
|
-
*
|
|
316
|
+
* tabbit/status 事件 → 聊天节点的 Definition(uiConversation 服务的契约
|
|
317
|
+
* 形状,同 dsh 文档 adding-a-conversation-node 的单事件业务写法)。
|
|
315
318
|
* 每次执行落一条整值事件,以 event.seq 为节点身份各自独立成行——重复
|
|
316
319
|
* 执行 /tabbit-info 天然多行并存、不互相覆盖;anchorSeq 用事件自身的
|
|
317
320
|
* seq,位置正好落在命令行节点(command/run)之后。start/update 都是纯
|
|
@@ -345,7 +348,7 @@ const tabbitStatusNode = {
|
|
|
345
348
|
|
|
346
349
|
module.exports = {
|
|
347
350
|
name: 'dsh-tabbit-client',
|
|
348
|
-
inject: ['inputTriggers', '
|
|
351
|
+
inject: ['inputTriggers', 'slots'],
|
|
349
352
|
apply(ctx) {
|
|
350
353
|
// 观看实例打点:告诉服务端"是哪个浏览器在看这个页面"。fire-and-forget
|
|
351
354
|
// (不 await、双层吞错)——纯属锦上添花的提示,任何失败都不能影响页面。
|
|
@@ -370,14 +373,19 @@ module.exports = {
|
|
|
370
373
|
}
|
|
371
374
|
}
|
|
372
375
|
|
|
373
|
-
// 功能三:/tabbit-info
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
+
// 功能三:/tabbit-info 状态卡。新宿主使用 uiConversation.events,
|
|
377
|
+
// 0.1.1 宿主使用 conversationEvents;只要任一接口存在就注册状态卡。
|
|
378
|
+
// 服务内部都是 ctx.effect 式注册(随插件 fiber 卸载自动回收),直接调。
|
|
379
|
+
const uiConversation = ctx.get('uiConversation');
|
|
376
380
|
const conversationEvents = ctx.get('conversationEvents');
|
|
377
381
|
const slots = ctx.get('slots');
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
conversationEvents
|
|
382
|
+
const registerStatusNode = typeof uiConversation?.events?.register === 'function'
|
|
383
|
+
? (definition) => uiConversation.events.register(definition)
|
|
384
|
+
: typeof conversationEvents?.register === 'function'
|
|
385
|
+
? (definition) => conversationEvents.register(definition)
|
|
386
|
+
: undefined;
|
|
387
|
+
if (registerStatusNode && slots && typeof slots.inject === 'function') {
|
|
388
|
+
registerStatusNode(tabbitStatusNode);
|
|
381
389
|
slots.inject('conversation.chat.node', () => slots.register(
|
|
382
390
|
{ name: 'conversation.chat.node', key: 'tabbit-status' },
|
|
383
391
|
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
|
-
|
|
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
|
|
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(
|
|
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 +
|
|
489
|
+
return { fields, body: source.slice(end + closing.length) };
|
|
476
490
|
}
|
|
477
491
|
/*
|
|
478
492
|
* 读盘 + 解析,组装 dsh 要的两样东西:候选卡片(list 用)与正文(get 用)。
|
package/lib/runtime/client.js
CHANGED
|
@@ -23,12 +23,15 @@
|
|
|
23
23
|
* - 任务【只能看到自己打开的或被显式 claim(认领)的标签页】,无法枚举
|
|
24
24
|
* 用户的其它标签页——这是浏览器侧的安全边界;
|
|
25
25
|
* - 整机最多 8 个并发任务;单次求值最长 120 秒;
|
|
26
|
-
* - 本客户端用到的 CLI 动词:`nodejs
|
|
27
|
-
* keep 语义两代有别,见 finishTask
|
|
28
|
-
* `checkpoint`(检查点)、`resource
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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`
|
|
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
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
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
|
|
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
|
-
...(
|
|
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.
|
|
3
|
+
"version": "0.3.3",
|
|
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": "
|
|
70
|
-
"@deepseek-ai/dsh-session": "
|
|
71
|
-
"@deepseek-ai/dsh-tools": "
|
|
72
|
-
"@deepseek-ai/dsh-web": "
|
|
73
|
-
"@deepseek-ai/schemastery": "link
|
|
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
|
},
|