tracup-bug 0.0.0-stage → 1.0.0

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/README.md CHANGED
@@ -1,3 +1,183 @@
1
- # Temporary Holding Version
1
+ # Tracup 提单工具 · 安装与使用(给同事)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 一句话:Tracup 免费版没有开放 API,这个工具用「浏览器登录态 + 网页同款接口」帮你建单。
4
+ 装好之后只有三个短命令,不用记路径、不用解压:`tracup login` / `tracup status` / `tracup create`;
5
+ 配合 AI agent(opencode / Claude)时,你只管说「把这个 bug 提到 Tracup」。
6
+
7
+ > 实测结论:官方 Open Interface 是付费功能;「用邮件创建任务」实测不建单(且 msn/outlook
8
+ > 邮箱自 2024-09 起发不出 SMTP)。别在这两条路上浪费时间。
9
+
10
+ ---
11
+
12
+ ## 一、安装(约 5 分钟,一次性)
13
+
14
+ ### 1. 装 Node.js(只做一次,以后不用管它是什么)
15
+
16
+ 到 https://nodejs.org 下载 LTS 版(绿色大按钮那个),一路「下一步」装完。
17
+ 装完不用做任何配置,回到这份文档继续第 2 步。
18
+
19
+ > 检查:打开终端(macOS 叫「终端」,Windows 叫 PowerShell),输入 `node -v`,
20
+ > 能显示版本号(如 `v22.x.x`)就是装好了。
21
+
22
+ ### 2. 装 tracup-bug(一条命令,依赖自动装好)
23
+
24
+ ```bash
25
+ npm install -g tracup-bug
26
+ ```
27
+
28
+ - 看到 `added N packages` 就是成功了。依赖是 npm 自动装的,**不用再跑别的安装脚本**。
29
+ - 如果公司内网下不了 npm 公开包,找我要 `tracup-bug-1.0.0.tgz`,然后跑:
30
+ `npm install -g tracup-bug-1.0.0.tgz`(效果一样)。
31
+
32
+ ### 3. 登录一次(必须在你自己的电脑、自己的终端里跑)
33
+
34
+ ```bash
35
+ tracup login
36
+ ```
37
+
38
+ 会弹出一个 Chrome 窗口,正常登录 Tracup(账密或扫码都行)。
39
+ 登录成功它自己检测到并关闭,终端打印 `✓ 登录态已保存`。
40
+
41
+ - 登录态存在你电脑的 `~/.cache/tracup/profile`,**通常能用数周**,过期重跑一次 `tracup login`。
42
+ - 这等价于你的 Tracup 会话:**别发给别人、别提交到 git**。
43
+ - 如果电脑没有 Chrome:跑 `tracup-setup --install-browser`(多下载约 150MB,只需一次)。
44
+
45
+ ### 4. 验证安装
46
+
47
+ ```bash
48
+ tracup status # 应显示 ✓ 登录态有效
49
+ tracup projects # 列出你能看到的项目/类型/成员
50
+ ```
51
+
52
+ 两条都过,就可以建单了。`projects` 的输出留着,后面 `--project/--type/--module` 照抄里面的名字。
53
+
54
+ ### 5. (可选)设默认项目
55
+
56
+ 第一次跑建单时会自动生成 `~/.config/tracup/web.json`,改成你常用的即可,省得每次都传:
57
+
58
+ ```json
59
+ { "project": "你们的项目名", "type": "bug", "status": "新建", "priority": 0, "assignee": "" }
60
+ ```
61
+
62
+ ---
63
+
64
+ ## 二、日常使用
65
+
66
+ ### 方式 A:直接跟 AI 说(推荐)
67
+
68
+ 装好 skill 后,在任何项目里跟 agent 说人话就行:
69
+
70
+ - 「把这个 bug 提到 Tracup」
71
+ - 「提交缺陷:库存列表按物品视图数量显示为 0」
72
+ - 「批量提 bug:……」(一条一条来,agent 会逐条建并汇总编号)
73
+
74
+ agent 会自己走 `status → projects → dry-run → 真建` 流程。**首次让它建单时,建议盯一眼
75
+ `--dry-run` 的报文**,确认项目/类型/优先级对了再放行。
76
+
77
+ ### 方式 B:自己敲命令
78
+
79
+ ```bash
80
+ # 1. 先看一眼报文(dry-run 不建单)
81
+ tracup create \
82
+ --title "库存列表按物品视图数量显示为 0" \
83
+ --body $'— 复现步骤 —\n1. 打开库存页\n2. 切到按物品\n\n— 期望 —\n显示 12\n\n— 实际 —\n显示 0' \
84
+ --project 你们的项目名 --type bug --priority 中 --dry-run
85
+
86
+ # 2. 确认无误,去掉 --dry-run 真建(建议加 --json)
87
+ tracup create --title "..." --body "..." --project 你们的项目名 --json
88
+ # 成功输出:✓ 已创建:项目名 #1234 标题
89
+ ```
90
+
91
+ 常用参数:`--title`(必填) `--body` `--project` `--type` `--module` `--assignee`
92
+ `--status` `--priority` `--dry-run` `--json`。
93
+
94
+ ### 优先级(只有三档,数字越大越低)
95
+
96
+ | 写法 | 界面显示 |
97
+ |---|---|
98
+ | `0` / `无` | 无(不设优先级) |
99
+ | `1` / `高` | 高 |
100
+ | `2` / `中` | 中 |
101
+ | `3` / `低` | 低 |
102
+
103
+ - **没有「紧急」档**:说「紧急」时传 `--priority 高`,并在标题/正文写明紧急原因。
104
+ - 别按 P0 的直觉传 `0`——`0` 是「无」,不是最高。
105
+
106
+ ### 建错了就删
107
+
108
+ ```bash
109
+ tracup delete 1234 # 只回显,不真删
110
+ tracup delete 1234 --yes # 真删
111
+ ```
112
+
113
+ **只能删自己建的测试单**,删别人的任务前必须先确认。
114
+
115
+ ---
116
+
117
+ ## 三、排错(按表查,不用猜)
118
+
119
+ | 现象 | 怎么办 |
120
+ |---|---|
121
+ | `找不到 playwright` | 重装包:`npm install -g tracup-bug`(或重跑 tgz 那条) |
122
+ | `浏览器启动失败` | 本机没 Chrome:跑 `tracup-setup --install-browser` |
123
+ | `登录态已失效` | 在**自己的终端**重跑 `tracup login`(agent 弹不出有头浏览器) |
124
+ | `项目里没有类型/模块/成员「xxx」` | 报错里会列出可选值,照抄;或先跑 `projects` 看 |
125
+ | `找不到项目「xxx」` | 同上;或改 `~/.config/tracup/web.json` 的默认项目 |
126
+ | HTTP 5xx / 超时 | Tracup 侧抖动,重试一次 |
127
+ | 建单成功但 Tracup 改版后突然全失败 | 接口是前端内部 API,对方改版会失效;找我,我按 skill 维护章节重抓接口 |
128
+
129
+ ---
130
+
131
+ ## 四、安全(3 条红线)
132
+
133
+ 1. `~/.config/tracup/`、`~/.cache/tracup/` **永不进 git、不外发**(里面是你的登录会话)。
134
+ 2. `capture` 录制文件可能含账号密码,只放本机,用完即删。
135
+ 3. 别把 `--assignee` 指给别人来冒充建单;指派只写自己或空着。
136
+
137
+ ---
138
+
139
+ ### 给 AI 用的同事(opencode / Claude Code 用户)
140
+
141
+ 如果你同事也用 AI 写代码,把 skill 目录再放一份到固定位置即可:
142
+
143
+ ```bash
144
+ # 解压 tracup-bug.zip 到 ~/.agents/skills/tracup-bug/(路径固定,别改),再补两个软链接:
145
+ ln -sfn ~/.agents/skills/tracup-bug ~/.config/opencode/skills/tracup-bug
146
+ ln -sfn ~/.agents/skills/tracup-bug ~/.claude/skills/tracup-bug
147
+ ```
148
+
149
+ 之后在任何项目里跟 AI 说「把这个 bug 提到 Tracup」就行。
150
+
151
+ ## 五、附:发给同事的话术模板(直接复制)
152
+
153
+ > 下面这段是给你转发/发群用的,改括号里的内容即可。
154
+
155
+ ---
156
+
157
+ **标题:Tracup 建单工具(5 分钟装好,以后跟 AI 说一句话就行)**
158
+
159
+ 背景:Tracup 免费版没有开放 API,邮件建单实测不可用。我这套是用浏览器登录态调网页同款接口,
160
+ (项目)已验证可建单,字段比邮件全(优先级/负责人/模块都能带)。
161
+
162
+ 安装(一次性,约 5 分钟):
163
+
164
+ 1. 装 Node.js:到 https://nodejs.org 下 LTS 版,一路下一步(装过可跳过,终端跑 `node -v` 能显示版本号就行)
165
+ 2. 终端跑 `npm install -g tracup-bug`(内网下不了找我拿 `tracup-bug-1.0.0.tgz`,跑 `npm install -g tracup-bug-1.0.0.tgz`)
166
+ 3. 终端跑 `tracup login`(弹浏览器,登录一次)
167
+ 4. 终端跑 `tracup status` 看到 `✓ 登录态有效` 即成功
168
+
169
+ 详细步骤见包里的 `README.md`。
170
+
171
+ 以后提 bug 两种方式:
172
+ - 跟 AI 说:「把这个 bug 提到 Tracup:(粘贴现象/复现步骤)」(需先按文档第 2 步装 skill)
173
+ - 或自己敲:`tracup create --title "…" --body "…" --project (你们的项目名) --dry-run` 先看报文,确认后去掉 `--dry-run` 真建。
174
+
175
+ 注意两点:
176
+ - 优先级只有 高/中/低 三档(`--priority 高`),没有「紧急」档,`0` 是「无」不是最高。
177
+ - 登录态能用数周,过期重跑一次 `login` 就行。
178
+
179
+ 有问题先看文档第三章排错表,还搞不定找我。
180
+
181
+ ---
182
+
183
+ *工具版本:1.0.0(2026-11-05,浏览器通道 + npm 包 + `--project/--priority语义/delete`)。Tracup 改版导致失效时找我,我按 skill 维护章节重抓接口。*
package/SKILL.md ADDED
@@ -0,0 +1,148 @@
1
+ ---
2
+ name: tracup-bug
3
+ description: "把 Bug / 缺陷提交到 Tracup。走持久化浏览器会话调用 Tracup 前端同款内部接口(Tracup 免费版无开放 API,且「邮件创建任务」实测不建单)。当用户说「把这个 bug 提到 Tracup」「提交缺陷」「记一条到 Tracup」「QA 提单」「批量提 bug」,或 QA 会话/复测结束需要建单时使用。不适用于 GitHub Issues / Jira / 飞书任务等其它跟踪器。"
4
+ ---
5
+
6
+ # Tracup 提单
7
+
8
+ ## 通道现状(2026-11-05 实测,别再走弯路)
9
+
10
+ | 通道 | 结论 |
11
+ |---|---|
12
+ | 官方开放接口(Open Interface) | ❌ 属 Enterprise 付费权益,免费版没有 |
13
+ | 「用邮件创建任务」 | ❌ 项目开关有、地址正确,但从**项目成员绑定邮箱**手动发信也不建单;且该绑定邮箱若是 msn/outlook 个人账号,Microsoft 2024-09 起已关闭 SMTP 基本认证,只能 OAuth2 |
14
+ | **浏览器会话调内部接口** | ✅ 已打通:`POST /api2/issue/create`(multipart) |
15
+
16
+ 工具:`~/.agents/skills/tracup-bug/bin/tracup_web.mjs`(Node + Playwright)。
17
+ 首次使用先跑一次 `bin/setup.mjs` 装依赖(只写 `~/.config/tracup`,不碰仓库、不需要 sudo)。
18
+
19
+ ```bash
20
+ BIN=~/.agents/skills/tracup-bug/bin
21
+ node $BIN/setup.mjs # 首次:装 playwright-core(配合本机 Chrome,约 5MB)
22
+ node $BIN/setup.mjs --install-browser # 本机没装 Chrome 时才需要(下载 Chromium 约 150MB)
23
+ ```
24
+
25
+ ## Step 0 · 检查登录态(每次先跑)
26
+
27
+ ```bash
28
+ BIN=~/.agents/skills/tracup-bug/bin
29
+ node $BIN/tracup_web.mjs status
30
+ ```
31
+
32
+ - 退出 0 → 继续。
33
+ - 退出 2 → 登录态过期。**必须让用户在自己的终端里跑**(agent 起不了有头窗口):
34
+
35
+ ```bash
36
+ node ~/.agents/skills/tracup-bug/bin/tracup_web.mjs login
37
+ ```
38
+
39
+ 登录态存在 `~/.cache/tracup/profile`(持久化浏览器用户目录),通常可维持数周。
40
+
41
+ `status` 同时做自检:playwright 能否加载、本机 Chrome 是否可用、配置文件是否存在,一行一行打印,排错先看它。
42
+
43
+ ## Step 1 · 确认项目与字段名
44
+
45
+ ```bash
46
+ node ~/.agents/skills/tracup-bug/bin/tracup_web.mjs projects
47
+ ```
48
+
49
+ 会列出该账号下每个项目的 `id / 类型名 / 状态名 / 成员名`。
50
+ **类型名、模块名、成员名写错会直接报错并给出可选值**,照提示改即可。
51
+
52
+ 默认项目取自 `~/.config/tracup/web.json` 的 `project`;临时换项目加 `--project 项目名` 即可,不用改配置。
53
+
54
+ ## Step 2 · 先 dry-run 看报文
55
+
56
+ ```bash
57
+ node ~/.agents/skills/tracup-bug/bin/tracup_web.mjs create \
58
+ --title "库存列表按物品视图数量显示为 0" \
59
+ --body $'— 复现步骤 —\n1. 打开库存页\n2. 切到按物品\n\n— 期望 —\n显示 12\n\n— 实际 —\n显示 0' \
60
+ --project smarterlab --type bug --priority 中 --module 采购 --assignee 周日 --dry-run
61
+ ```
62
+
63
+ ## Step 3 · 确认后真建
64
+
65
+ 去掉 `--dry-run`,建议加 `--json` 便于汇总:
66
+
67
+ ```bash
68
+ node ~/.agents/skills/tracup-bug/bin/tracup_web.mjs create --title "..." --body "..." --json
69
+ ```
70
+
71
+ 成功输出:`✓ 已创建:smarterlab #6599 标题` + `id / priority=1(高) / status / type`。
72
+
73
+ ## Step 4 · 建错了就删(可选)
74
+
75
+ ```bash
76
+ node ~/.agents/skills/tracup-bug/bin/tracup_web.mjs delete 6599 # 先只回显,不真删
77
+ node ~/.agents/skills/tracup-bug/bin/tracup_web.mjs delete 6599 --yes # 真删
78
+ ```
79
+
80
+ 支持 `#6599` / `6599` / 32 位任务 id 三种写法(编号会先经 `issue/info` 解析成 id)。
81
+ **删除前必须先跟用户确认**,别自作主张删别人建的任务。
82
+
83
+ ## 参数
84
+
85
+ | 参数 | 说明 |
86
+ |---|---|
87
+ | `--title` | 必填。一句话写清「哪里 + 什么现象」,别用「bug」「问题」开头 |
88
+ | `--body` | 多行纯文本,自动按行转成段落节点(Tracup 描述是富文本 JSON) |
89
+ | `--project` | 项目名或 id;不传则用 `~/.config/tracup/web.json` 的 `project` |
90
+ | `--priority` | **`0`=无 `1`=高 `2`=中 `3`=低**(也可直接写 `无/高/中/低`)。默认取配置文件的 `priority` |
91
+ | `--type` | 项目类型名,默认 `bug` |
92
+ | `--module` | 模块名,项目里没配就留空 |
93
+ | `--assignee` | 成员名或邮箱,默认不指派 |
94
+ | `--status` | 状态名,默认项目里 `markAsCreated` 的那个(通常「新建」) |
95
+ | `--dry-run` | 只打印将提交的报文,不建单 |
96
+ | `--json` | 机器可读输出 |
97
+
98
+ ### ⚠️ 优先级只有三档,且数字越大越低
99
+
100
+ Tracup 前端词条原文(`label.priority1/2/3`):**1=高(High)、2=中(Normal)、3=低(Low)**,`0` = 不设优先级。
101
+
102
+ - **没有「紧急/最高/P0」**。用户说「紧急」时按 `高`(1) 提交,并在汇报里说明降级了。
103
+ - **别按 P0 最紧急的直觉反着用**:`--priority 0` 不是最高,是「无」。
104
+ - 想要「紧急」语义,正确做法是 `--priority 高` + 在标题/正文里写明紧急原因,不要自己造档位。
105
+
106
+ 默认值集中在 `~/.config/tracup/web.json`(`setup.mjs` 已生成一份,可直接改):
107
+
108
+ ```json
109
+ { "project": "smarterlab", "type": "bug", "status": "新建", "priority": 0, "assignee": "" }
110
+ ```
111
+
112
+ ## 批量(QA 会话场景)
113
+
114
+ 一条 bug 一条命令,**不要合并**。逐条执行,收集每条的 `#编号`,最后汇总成表格给用户;
115
+ 某条失败就记下原因继续下一条,不要中断整批。
116
+
117
+ ## 铁律
118
+
119
+ - **先 dry-run 再真建**;用户明确说「直接建」才跳过确认。
120
+ - **一条 bug 一条任务**,合并会丢掉单条可跟踪性。
121
+ - **别塞整段日志/diff**:正文放「现象 + 最小复现」,长内容截关键片段。
122
+ - **不提交凭据**:`~/.config/tracup/`、`~/.cache/tracup/`(含登录 profile 与接口录制)永不进 git。
123
+ - **建单前先看列表**:同一问题别建两条。
124
+ - **删除只删自己建的测试单**,且先跑不带 `--yes` 的那次把目标回显给用户看。
125
+
126
+ ## 排错
127
+
128
+ | 现象 | 处理 |
129
+ |---|---|
130
+ | `找不到 playwright` | 跑 `node ~/.agents/skills/tracup-bug/bin/setup.mjs` |
131
+ | `浏览器启动失败` | 本机没 Chrome:跑 `setup.mjs --install-browser` |
132
+ | `登录态可能已失效,请重跑 login` | agent 跑不了有头窗口,让用户在自己终端跑 `login` |
133
+ | `项目里没有类型「xxx」` | 报错里会列可选值,照抄 |
134
+ | `项目里没有模块「xxx」` | 该模块不存在,去掉 `--module` |
135
+ | `项目里没有成员「xxx」` | 同上,去掉 `--assignee` |
136
+ | `找不到项目「xxx」` | 报错里会列该账号下所有项目名,照抄;或改 `web.json` |
137
+ | HTTP 5xx / timeout | Tracup 侧抖动,重试一次 |
138
+
139
+ ## 维护
140
+
141
+ 接口是对接 Tracup 前端内部 API,**对方改版会失效**。失效时按此流程重抓:
142
+
143
+ ```bash
144
+ node ~/.agents/skills/tracup-bug/bin/tracup_web.mjs capture --duration 120
145
+ # 在打开窗口里手动建一条任务,然后看 ~/.cache/tracup/capture-*.jsonl 里 POST /api2/issue/create 的报文
146
+ ```
147
+
148
+ `node ~/.agents/skills/tracup-bug/bin/tracup_web.mjs api GET /api2/<path>` 可复用登录态直接探接口。
package/bin/setup.mjs ADDED
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * tracup-bug 初始化:装依赖 + 落默认配置 + 自检
4
+ *
5
+ * node setup.mjs # 装 playwright(轻量,用本机 Chrome)
6
+ * node setup.mjs --install-browser # 顺带下载 playwright 自带 Chromium(没装 Chrome 时用)
7
+ * node setup.mjs --full # 装完整 playwright(自动下载浏览器,最稳但最慢)
8
+ *
9
+ * 只写用户目录(~/.config/tracup),不碰仓库、不需要 sudo。
10
+ */
11
+
12
+ import fs from "node:fs";
13
+ import os from "node:os";
14
+ import path from "node:path";
15
+ import { createRequire } from "node:module";
16
+ import { execFileSync } from "node:child_process";
17
+
18
+ const CONFIG_DIR = path.join(os.homedir(), ".config", "tracup");
19
+ const RUNTIME_DIR = path.join(CONFIG_DIR, "runtime");
20
+ const WEB_CONFIG_PATH = path.join(CONFIG_DIR, "web.json");
21
+ const require = createRequire(import.meta.url);
22
+
23
+ const args = new Set(process.argv.slice(2));
24
+ const wantBrowser = args.has("--install-browser") || args.has("--full");
25
+ const fullPlaywright = args.has("--full");
26
+
27
+ function log(...xs) {
28
+ console.log(...xs);
29
+ }
30
+
31
+ function npmBin() {
32
+ return process.platform === "win32" ? "npm.cmd" : "npm";
33
+ }
34
+
35
+ function run(cmd, cmdArgs, cwd) {
36
+ log(`$ ${cmd} ${cmdArgs.join(" ")} (cwd=${cwd})`);
37
+ execFileSync(cmd, cmdArgs, { cwd, stdio: "inherit", shell: process.platform === "win32" });
38
+ }
39
+
40
+ /** 已经能解析到 playwright 就跳过安装。 */
41
+ function findPlaywright() {
42
+ const candidates = [
43
+ process.env.TRACUP_PLAYWRIGHT_DIR,
44
+ RUNTIME_DIR,
45
+ path.join(CONFIG_DIR, "node_modules"),
46
+ ].filter(Boolean);
47
+ for (const pkg of ["playwright", "playwright-core"]) {
48
+ for (const base of candidates) {
49
+ try {
50
+ return require.resolve(pkg, { paths: [base] });
51
+ } catch {
52
+ /* 下一个 */
53
+ }
54
+ }
55
+ try {
56
+ return require.resolve(pkg);
57
+ } catch {
58
+ /* 下一个包名 */
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ function writeDefaultConfig() {
65
+ if (fs.existsSync(WEB_CONFIG_PATH)) {
66
+ log(`· 已存在配置,保持不动:${WEB_CONFIG_PATH}`);
67
+ return;
68
+ }
69
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
70
+ const defaults = {
71
+ project: "smarterlab",
72
+ type: "bug",
73
+ status: "新建",
74
+ priority: 0,
75
+ assignee: "",
76
+ };
77
+ fs.writeFileSync(WEB_CONFIG_PATH, JSON.stringify(defaults, null, 2) + "\n", "utf8");
78
+ log(`✓ 已写入默认配置:${WEB_CONFIG_PATH}`);
79
+ }
80
+
81
+ function main() {
82
+ log("=== tracup-bug 初始化 ===");
83
+ log(`node ${process.version} · ${process.platform}/${process.arch}`);
84
+ log("");
85
+
86
+ const existing = findPlaywright();
87
+ if (existing) {
88
+ log(`✓ 已找到 playwright:${existing}(跳过安装)`);
89
+ } else {
90
+ fs.mkdirSync(RUNTIME_DIR, { recursive: true });
91
+ const pkgPath = path.join(RUNTIME_DIR, "package.json");
92
+ if (!fs.existsSync(pkgPath)) {
93
+ fs.writeFileSync(
94
+ pkgPath,
95
+ JSON.stringify({ name: "tracup-bug-runtime", private: true, version: "1.0.0" }, null, 2) + "\n",
96
+ "utf8"
97
+ );
98
+ }
99
+ // playwright-core 不带浏览器(~5MB,配合本机 Chrome 即可);--full 用完整包
100
+ const pkg = fullPlaywright ? "playwright" : "playwright-core";
101
+ log(`\n安装 ${pkg} 到 ${RUNTIME_DIR} …`);
102
+ run(npmBin(), ["install", pkg, "--no-audit", "--no-fund"], RUNTIME_DIR);
103
+ const found = findPlaywright();
104
+ if (!found) {
105
+ log("\n✗ 装完仍解析不到 playwright,请把上面的 npm 报错发给我。");
106
+ process.exit(1);
107
+ }
108
+ log(`✓ playwright 就位:${found}`);
109
+ }
110
+
111
+ if (wantBrowser) {
112
+ log("\n下载 playwright 自带 Chromium(约 150MB,只需一次)…");
113
+ run(npmBin(), ["exec", "--yes", "--", "playwright", "install", "chromium"], RUNTIME_DIR);
114
+ log("✓ 浏览器就位");
115
+ } else {
116
+ log("\n· 未下载 Chromium:将使用本机 Google Chrome(macOS/Windows 一般都装了)");
117
+ log(` 若本机没有 Chrome,跑:node ${path.join(path.dirname(new URL(import.meta.url).pathname), "setup.mjs")} --install-browser`);
118
+ }
119
+
120
+ writeDefaultConfig();
121
+
122
+ log("\n=== 下一步:登录一次 ===");
123
+ log("必须在你自己的终端里跑(会弹出有头浏览器,agent 起不了窗口):");
124
+ log(` node ${path.join(path.dirname(new URL(import.meta.url).pathname), "tracup_web.mjs")} login`);
125
+ log("\n登录态存在 ~/.cache/tracup/profile,通常能用数周;过期重跑 login 即可。");
126
+ }
127
+
128
+ main();
@@ -0,0 +1,590 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * tracup-web —— 用真实浏览器操作 Tracup(免费版没有开放 API,只能走网页同款内部接口)
4
+ *
5
+ * 通道结论(2026-11 实测,别再重走):
6
+ * 官方 Open Interface 属 Enterprise 付费权益 → 免费版没有
7
+ * 「用邮件创建任务」实测不建单(成员绑定邮箱手动发信也不建),且 msn/outlook
8
+ * 个人账号自 2024-09-16 起停用 SMTP 基本认证 → 邮件通道彻底放弃
9
+ * ✅ 浏览器会话 + POST /api2/issue/create(前端自己用的接口,字段最全)
10
+ *
11
+ * 用法:node tracup_web.mjs <login|status|projects|create|delete|capture|api>
12
+ * login 打开有头浏览器,你手动登录一次(登录态存 ~/.cache/tracup/profile,可用数周)
13
+ * status 检查登录态 + 自检(playwright / 浏览器 / 配置是否就位)
14
+ * projects 列出项目及其类型、状态、模块、成员名
15
+ * create 建单(--title/--body/--priority/--type/--module/--assignee/--status/--dry-run/--json)
16
+ * delete 删单(delete <编号|id> --yes)
17
+ * capture 录制网页操作请求(接口改版时重新抓报文)
18
+ * api 复用登录态直接探内部接口
19
+ *
20
+ * 依赖解析:不写死 node_modules 位置,依次尝试
21
+ * $TRACUP_PLAYWRIGHT_DIR / ~/.config/tracup/runtime / 全局 npm / 脚本同级目录
22
+ * 浏览器:优先用 playwright 自带 chromium,没下载就自动退回本机 Chrome(channel: "chrome")
23
+ */
24
+
25
+ import fs from "node:fs";
26
+ import os from "node:os";
27
+ import path from "node:path";
28
+ import readline from "node:readline";
29
+ import { createRequire } from "node:module";
30
+ import { fileURLToPath } from "node:url";
31
+ import { execFileSync } from "node:child_process";
32
+
33
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
34
+ const SKILL_DIR = path.resolve(__dirname, "..");
35
+ const CONFIG_DIR = path.join(os.homedir(), ".config", "tracup");
36
+ const RUNTIME_DIR = path.join(CONFIG_DIR, "runtime");
37
+ const CACHE_DIR = path.join(os.homedir(), ".cache", "tracup");
38
+ const PROFILE_DIR = path.join(CACHE_DIR, "profile");
39
+ const WEB_CONFIG_PATH = path.join(CONFIG_DIR, "web.json");
40
+ const APP_URL = "https://app.tracup.com/";
41
+ const API_ORIGIN = "https://www.tracup.com";
42
+ const LOGIN_HINT = /login|signin|sign-in|passport|auth/i;
43
+
44
+ const require = createRequire(import.meta.url);
45
+
46
+ /** 依次尝试若干位置加载 playwright(兼容全局安装、本地 runtime、仓内 node_modules)。 */
47
+ function loadPlaywright() {
48
+ const candidates = [
49
+ process.env.TRACUP_PLAYWRIGHT_DIR,
50
+ RUNTIME_DIR,
51
+ path.join(CONFIG_DIR, "node_modules"),
52
+ SKILL_DIR,
53
+ process.cwd(),
54
+ ].filter(Boolean);
55
+
56
+ const tried = [];
57
+ // playwright(自带浏览器)与 playwright-core(配合本机 Chrome)都认
58
+ for (const pkg of ["playwright", "playwright-core"]) {
59
+ for (const base of candidates) {
60
+ try {
61
+ const resolved = require.resolve(pkg, { paths: [base] });
62
+ return { mod: require(resolved), from: resolved };
63
+ } catch (err) {
64
+ tried.push(`${pkg} @ ${base} (${err.code || err.message})`);
65
+ }
66
+ }
67
+ try {
68
+ return { mod: require(pkg), from: require.resolve(pkg) };
69
+ } catch { /* 下一个包名 */ }
70
+ }
71
+
72
+ throw new Error(
73
+ "找不到 playwright / playwright-core。请先跑一次初始化:\n" +
74
+ ` node ${path.join(SKILL_DIR, "bin", "setup.mjs")}\n` +
75
+ `已经试过这些位置:\n - ${tried.join("\n - ")}`
76
+ );
77
+ }
78
+
79
+ let _pw = null;
80
+ function playwright() {
81
+ if (!_pw) _pw = loadPlaywright();
82
+ return _pw;
83
+ }
84
+
85
+ /** 有内置 chromium 就用;没下载过(常见于只装了 playwright-core)就退回本机 Chrome。 */
86
+ async function launchBrowser(profileDir, opts) {
87
+ const { chromium } = playwright().mod;
88
+ fs.mkdirSync(profileDir, { recursive: true });
89
+ try {
90
+ return await chromium.launchPersistentContext(profileDir, opts);
91
+ } catch (err) {
92
+ const msg = String(err && err.message);
93
+ if (!/Executable doesn't exist|is not found|Failed to launch|browserType\.launch/i.test(msg)) throw err;
94
+ log("(未找到 playwright 自带 Chromium,改用本机 Chrome)");
95
+ return chromium.launchPersistentContext(profileDir, { ...opts, channel: "chrome" });
96
+ }
97
+ }
98
+
99
+ function log(...args) {
100
+ console.log(...args);
101
+ }
102
+
103
+ async function openContext({ headless }) {
104
+ return launchBrowser(PROFILE_DIR, {
105
+ headless,
106
+ viewport: { width: 1440, height: 900 },
107
+ acceptDownloads: true,
108
+ args: ["--disable-blink-features=AutomationControlled"],
109
+ });
110
+ }
111
+
112
+ function waitEnter(promptText) {
113
+ return new Promise((resolve) => {
114
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
115
+ rl.question(promptText, () => {
116
+ rl.close();
117
+ resolve("enter");
118
+ });
119
+ });
120
+ }
121
+
122
+ /** 轮询判断是否已进入应用(不是登录页且有会话 cookie)。 */
123
+ async function waitForLogin(page, timeoutMs = 300000) {
124
+ const started = Date.now();
125
+ while (Date.now() - started < timeoutMs) {
126
+ await page.waitForTimeout(1500);
127
+ let url = "";
128
+ try {
129
+ url = page.url();
130
+ } catch {
131
+ continue;
132
+ }
133
+ if (!url || LOGIN_HINT.test(url.replace(APP_URL, ""))) continue;
134
+ const cookies = await page.context().cookies();
135
+ const hasSession = cookies.some((c) => c.value && c.value.length > 20 && !/^(1|0|true|false)$/i.test(c.value));
136
+ if (hasSession) return "auto";
137
+ }
138
+ return "timeout";
139
+ }
140
+
141
+ async function cmdLogin() {
142
+ const headless = process.env.TRACUP_WEB_HEADLESS === "1";
143
+ const ctx = await openContext({ headless });
144
+ const page = ctx.pages()[0] || (await ctx.newPage());
145
+ await page.goto(APP_URL, { waitUntil: "domcontentloaded" });
146
+
147
+ log("浏览器已打开,请在窗口里完成 Tracup 登录(支持扫码/账密)。");
148
+ log("登录成功后会自动继续;也可以回到这里按回车。\n");
149
+
150
+ const result = process.stdin.isTTY
151
+ ? await Promise.race([waitForLogin(page), waitEnter("登录完成后按回车继续… ")])
152
+ : await waitForLogin(page);
153
+ if (result === "timeout") {
154
+ log("⚠️ 5 分钟内没检测到登录态,已放弃。请重跑 login。");
155
+ await ctx.close();
156
+ process.exit(1);
157
+ }
158
+
159
+ await page.waitForTimeout(1000);
160
+ log(`✓ 登录态已保存到 ${PROFILE_DIR}`);
161
+ log(` 当前页面:${page.url()}`);
162
+ if (!headless) await ctx.close();
163
+ else await ctx.close();
164
+ }
165
+
166
+ /** 自检:依赖 / 配置 / 登录态。排错时先跑这个,比猜快。 */
167
+ async function cmdStatus() {
168
+ let pw;
169
+ try {
170
+ pw = playwright();
171
+ log(`✓ playwright:${pw.from}`);
172
+ } catch (err) {
173
+ log(`✗ ${err.message}`);
174
+ process.exit(3);
175
+ }
176
+
177
+ log(
178
+ fs.existsSync(WEB_CONFIG_PATH)
179
+ ? `✓ 配置:${WEB_CONFIG_PATH}`
180
+ : `· 配置:${WEB_CONFIG_PATH} 不存在(会用内置默认:smarterlab / bug / 新建 / 无)`
181
+ );
182
+
183
+ let ctx;
184
+ try {
185
+ ctx = await openContext({ headless: true });
186
+ } catch (err) {
187
+ log(`✗ 浏览器启动失败:${err.message}`);
188
+ log(` 排错:node ${path.join(SKILL_DIR, "bin", "setup.mjs")} --install-browser`);
189
+ process.exit(3);
190
+ }
191
+
192
+ const page = ctx.pages()[0] || (await ctx.newPage());
193
+ await page.goto(APP_URL, { waitUntil: "domcontentloaded" });
194
+ await page.waitForTimeout(3000);
195
+ const url = page.url();
196
+ const ok = !LOGIN_HINT.test(url.replace(APP_URL, ""));
197
+ log(ok ? `✓ 登录态有效(${url})` : `✗ 登录态已失效(${url})`);
198
+ await ctx.close();
199
+
200
+ if (!ok) {
201
+ log(`\n请在你自己的终端里跑(会弹有头浏览器,agent 起不了窗口):`);
202
+ log(` node ${path.join(SKILL_DIR, "bin", "tracup_web.mjs")} login`);
203
+ }
204
+ process.exit(ok ? 0 : 2);
205
+ }
206
+
207
+ /**
208
+ * 录制请求:把你在网页上做操作时发出的 XHR/fetch 全量落盘,
209
+ * 用来反推「创建任务」的内部接口与报文格式(比抠 DOM 选择器稳得多)。
210
+ */
211
+ async function cmdCapture(args) {
212
+ const out = args.out || path.join(CACHE_DIR, `capture-${Date.now()}.jsonl`);
213
+ fs.mkdirSync(path.dirname(out), { recursive: true });
214
+ const stream = fs.createWriteStream(out, { flags: "a" });
215
+
216
+ const ctx = await openContext({ headless: false });
217
+ const page = ctx.pages()[0] || (await ctx.newPage());
218
+
219
+ const seen = new Set();
220
+ const record = (entry) => {
221
+ const key = `${entry.method} ${entry.url} ${entry.postData || ""}`;
222
+ if (seen.has(key) && entry.status === undefined) return;
223
+ if (entry.type !== "xhr" && entry.type !== "fetch") return;
224
+ stream.write(JSON.stringify(entry) + "\n");
225
+ };
226
+
227
+ ctx.on("request", (req) => {
228
+ const type = req.resourceType();
229
+ if (type !== "xhr" && type !== "fetch") return;
230
+ record({ ts: Date.now(), phase: "request", method: req.method(), url: req.url(), postData: req.postData() || null, type });
231
+ });
232
+ ctx.on("response", async (resp) => {
233
+ const req = resp.request();
234
+ const type = req.resourceType();
235
+ if (type !== "xhr" && type !== "fetch") return;
236
+ let body = null;
237
+ try {
238
+ const ct = resp.headers()["content-type"] || "";
239
+ if (ct.includes("json") && resp.status() < 400) body = (await resp.text()).slice(0, 4000);
240
+ } catch {
241
+ /* 响应体读取失败不影响录制 */
242
+ }
243
+ record({ ts: Date.now(), phase: "response", method: req.method(), url: req.url(), postData: req.postData() || null, status: resp.status(), body, type });
244
+ });
245
+
246
+ await page.goto(args.url || APP_URL, { waitUntil: "domcontentloaded" });
247
+ log(`开始录制 → ${out}`);
248
+ log("请在浏览器里完整地手动创建一条任务(标题随便写),完成后回来按回车停止。\n");
249
+ if (process.stdin.isTTY) {
250
+ await waitEnter("按回车停止录制… ");
251
+ } else {
252
+ const seconds = Number(args.duration || 180);
253
+ log(`(非交互模式,${seconds} 秒后自动停止)`);
254
+ await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
255
+ }
256
+ stream.end();
257
+ await ctx.close();
258
+ log(`✓ 录制完成:${out}`);
259
+ }
260
+
261
+ /**
262
+ * 通用接口调用:复用浏览器登录态直接打 Tracup 内部 API。
263
+ * 用于探查/调试(例如查项目下的状态、类型、优先级选项)。
264
+ */
265
+ async function cmdApi(args) {
266
+ const method = (args._[1] || "GET").toUpperCase();
267
+ const rawPath = args._[2];
268
+ if (!rawPath) throw new Error("用法:node tracup_web.mjs api <METHOD> <path> [--form '{...}'] [--limit 2000]");
269
+ const url = rawPath.startsWith("http") ? rawPath : `https://www.tracup.com${rawPath}`;
270
+
271
+ const ctx = await openContext({ headless: true });
272
+ const page = ctx.pages()[0] || (await ctx.newPage());
273
+ await page.goto(APP_URL, { waitUntil: "domcontentloaded" });
274
+ await page.waitForTimeout(2000);
275
+
276
+ let resp;
277
+ if (args.form) {
278
+ resp = await ctx.request.post(url, { multipart: JSON.parse(args.form) });
279
+ } else if (args.body) {
280
+ resp = await ctx.request.fetch(url, {
281
+ method,
282
+ headers: { "content-type": "application/json" },
283
+ data: args.body,
284
+ });
285
+ } else {
286
+ resp = await ctx.request.fetch(url, { method });
287
+ }
288
+
289
+ const text = await resp.text();
290
+ log(`HTTP ${resp.status()}`);
291
+ const limit = Number(args.limit || 2000);
292
+ log(text.length > limit ? text.slice(0, limit) + `\n…(共 ${text.length} 字符)` : text);
293
+ await ctx.close();
294
+ process.exit(resp.status() >= 400 ? 2 : 0);
295
+ }
296
+
297
+ /** 读取 web 配置(项目/类型/状态/默认负责人),缺省回退合理值。 */
298
+ function loadWebConfig() {
299
+ const p = path.join(os.homedir(), ".config", "tracup", "web.json");
300
+ if (!fs.existsSync(p)) return { configPath: p, exists: false };
301
+ try {
302
+ return { ...JSON.parse(fs.readFileSync(p, "utf8")), configPath: p, exists: true };
303
+ } catch (err) {
304
+ throw new Error(`配置文件不是合法 JSON:${p}(${err.message})`);
305
+ }
306
+ }
307
+
308
+ async function fetchWorkspace(ctx) {
309
+ const resp = await ctx.request.get("https://www.tracup.com/api2/workspace/config");
310
+ if (resp.status() !== 200) {
311
+ throw new Error(`拉取工作区失败(HTTP ${resp.status()}),登录态可能已失效,请重跑 login`);
312
+ }
313
+ return resp.json();
314
+ }
315
+
316
+ function pickProject(config, webConfig) {
317
+ const projects = (config.projects || []).filter((p) => !p.archived);
318
+ if (!projects.length) throw new Error("账号下没有可用项目");
319
+ const want = webConfig.project;
320
+ if (!want) return projects.find((p) => p.name === "smarterlab") || projects[0];
321
+ const hit = projects.find((p) => p.id === want || p.name === want);
322
+ if (!hit) {
323
+ throw new Error(
324
+ `找不到项目「${want}」,可用:${projects.map((p) => p.name).join(" / ")}`
325
+ );
326
+ }
327
+ return hit;
328
+ }
329
+
330
+ function pickByName(list, name, kind) {
331
+ if (!name) return null;
332
+ const hit = (list || []).find((x) => x.name === name || x.id === name);
333
+ if (!hit) {
334
+ throw new Error(`项目里没有${kind}「${name}」,可用:${(list || []).map((x) => x.name).join(" / ")}`);
335
+ }
336
+ return hit;
337
+ }
338
+
339
+ function pickMember(project, name) {
340
+ if (!name) return null;
341
+ const members = (project.members || []).filter((m) => !m.isQuit);
342
+ const hit = members.find((m) => m.name === name || m.email === name || m.id === name);
343
+ if (!hit) {
344
+ throw new Error(`项目里没有成员「${name}」,可用:${members.map((m) => m.name).join(" / ")}`);
345
+ }
346
+ return hit;
347
+ }
348
+
349
+ /** 纯文本 → Tracup 的富文本描述结构(Plate/Slate 风格段落数组)。 */
350
+ function textToDescription(body) {
351
+ const text = String(body || "").replace(/\r\n/g, "\n").trim();
352
+ const lines = text ? text.split("\n") : [""];
353
+ return JSON.stringify(lines.map((line) => ({ type: "paragraph", children: [{ text: line }] })));
354
+ }
355
+
356
+ /**
357
+ * Tracup 优先级:数字 → 界面文案。
358
+ * 取自前端 bundle 的 zh-CN 词条(priority1/2/3 + 无):
359
+ * 1 = 高 / High,2 = 中 / Normal,3 = 低 / Low,0 = 无(不设)
360
+ * 注意:数字越大优先级越低,别按「P0 最紧急」的直觉反着用。
361
+ */
362
+ const PRIORITY_LABELS = { 0: "无", 1: "高", 2: "中", 3: "低" };
363
+ const PRIORITY_ALIASES = {
364
+ "无": 0, none: 0,
365
+ "高": 1, high: 1, "紧急": 1, urgent: 1,
366
+ "中": 2, normal: 2, medium: 2, "普通": 2, "一般": 2,
367
+ "低": 3, low: 3,
368
+ };
369
+
370
+ function normalizePriority(raw) {
371
+ if (raw === undefined || raw === null || raw === "") return 0;
372
+ const s = String(raw).trim();
373
+ if (/^[0-3]$/.test(s)) return Number(s);
374
+ const key = s.toLowerCase();
375
+ const hit = PRIORITY_ALIASES[key] ?? PRIORITY_ALIASES[s];
376
+ if (hit !== undefined) {
377
+ if ((s === "紧急" || key === "urgent")) {
378
+ log("提示:Tracup 只有「高/中/低」三档,没有「紧急」,「紧急」按「高」提交。");
379
+ }
380
+ return hit;
381
+ }
382
+ throw new Error(`无效的优先级「${raw}」,可用:0/1/2/3 或 无/高/中/低(Tracup 只有三档,1=高 2=中 3=低)`);
383
+ }
384
+
385
+ async function cmdProjects() {
386
+ const ctx = await openContext({ headless: true });
387
+ const page = ctx.pages()[0] || (await ctx.newPage());
388
+ await page.goto(APP_URL, { waitUntil: "domcontentloaded" });
389
+ const ws = await fetchWorkspace(ctx);
390
+ for (const p of ws.projects || []) {
391
+ if (p.archived) continue;
392
+ log(`\n● ${p.name}${p.mailboxActiveStatus ? "(邮件建单已开)" : ""}`);
393
+ log(` id: ${p.id}`);
394
+ log(` 类型: ${(p.types || []).map((t) => t.name).join(" / ")}`);
395
+ log(` 状态: ${(p.status || []).map((s) => s.name + (s.markAsCreated ? "(默认)" : "")).join(" / ")}`);
396
+ log(` 成员: ${(p.members || []).filter((m) => !m.isQuit).map((m) => m.name).join(" / ")}`);
397
+ }
398
+ await ctx.close();
399
+ }
400
+
401
+ async function cmdCreate(args) {
402
+ const title = (args.title || "").trim();
403
+ if (!title) throw new Error("缺少 --title");
404
+ const webConfig = loadWebConfig();
405
+
406
+ const ctx = await openContext({ headless: true });
407
+ const page = ctx.pages()[0] || (await ctx.newPage());
408
+ await page.goto(APP_URL, { waitUntil: "domcontentloaded" });
409
+
410
+ const ws = await fetchWorkspace(ctx);
411
+ const project = pickProject(ws, { ...webConfig, project: args.project || webConfig.project });
412
+
413
+ const status =
414
+ pickByName(project.status, args.status || webConfig.status, "状态") ||
415
+ (project.status || []).find((s) => s.markAsCreated) ||
416
+ (project.status || [])[0];
417
+ const type =
418
+ pickByName(project.types, args.type || webConfig.type || "bug", "类型") || (project.types || [])[0];
419
+ const module = pickByName(project.modules, args.module || webConfig.module, "模块");
420
+ const assignee = pickMember(project, args.assignee || webConfig.assignee);
421
+
422
+ const priority = normalizePriority(args.priority ?? webConfig.priority ?? 0);
423
+
424
+ const form = {
425
+ project: project.id,
426
+ "data[status]": status.id,
427
+ "data[title]": title,
428
+ "data[assignee]": assignee ? assignee.id : "",
429
+ "data[description]": textToDescription(args.body || ""),
430
+ "data[priority]": String(priority),
431
+ "data[type]": type.id,
432
+ "data[module]": module ? module.id : "",
433
+ "data[version]": "",
434
+ "data[started]": "0",
435
+ "data[deadline]": "0",
436
+ "data[mode]": "1",
437
+ "data[customField]": "{}",
438
+ };
439
+
440
+ if (args["dry-run"]) {
441
+ log(JSON.stringify({ project: project.name, projectId: project.id, form }, null, 2));
442
+ await ctx.close();
443
+ return;
444
+ }
445
+
446
+ const resp = await ctx.request.post("https://www.tracup.com/api2/issue/create", { multipart: form });
447
+ const text = await resp.text();
448
+ let data;
449
+ try {
450
+ data = JSON.parse(text);
451
+ } catch {
452
+ throw new Error(`接口返回非 JSON(HTTP ${resp.status()}):${text.slice(0, 300)}`);
453
+ }
454
+ await ctx.close();
455
+
456
+ if (data.code !== undefined && data.code !== 0) {
457
+ throw new Error(`创建失败:${data.message || text.slice(0, 300)}`);
458
+ }
459
+ // 接口有时返回 {code:0,data:{...}} 信封,有时直接返回任务对象,两种都认
460
+ const task = data.data && data.data.id ? data.data : data.id ? data : null;
461
+ if (!task) throw new Error(`创建失败,响应里没有任务:${text.slice(0, 300)}`);
462
+ const taskPriority = task.priority ?? priority;
463
+ if (args.json) {
464
+ log(
465
+ JSON.stringify(
466
+ {
467
+ ok: true,
468
+ number: task.number,
469
+ id: task.id,
470
+ title: task.title,
471
+ project: project.name,
472
+ priority: taskPriority,
473
+ priorityLabel: PRIORITY_LABELS[taskPriority] ?? "",
474
+ },
475
+ null,
476
+ 2
477
+ )
478
+ );
479
+ } else {
480
+ log(`✓ 已创建:${project.name} #${task.number} ${task.title}`);
481
+ log(
482
+ ` id=${task.id} priority=${taskPriority}(${PRIORITY_LABELS[taskPriority] ?? "未知"}) status=${status.name} type=${type.name}`
483
+ );
484
+ }
485
+ }
486
+
487
+ /**
488
+ * 删除任务:POST /api2/issue/delete,字段 project + id。
489
+ * 会真删(前端确认弹窗之后走的就是它),所以默认要求 --yes。
490
+ */
491
+ async function cmdDelete(args) {
492
+ const idOrNumber = args.id || args._[1];
493
+ if (!idOrNumber) throw new Error("用法:node tracup_web.mjs delete <任务id或编号> [--project 名称] [--yes]");
494
+
495
+ const webConfig = loadWebConfig();
496
+ const ctx = await openContext({ headless: true });
497
+ const page = ctx.pages()[0] || (await ctx.newPage());
498
+ await page.goto(APP_URL, { waitUntil: "domcontentloaded" });
499
+
500
+ const ws = await fetchWorkspace(ctx);
501
+ const project = pickProject(ws, { ...webConfig, project: args.project || webConfig.project });
502
+
503
+ // 允许传 #6597 / 6597 这样的编号,先按编号查到真实 id
504
+ let taskId = String(idOrNumber).replace(/^#/, "");
505
+ const isObjectId = /^[0-9a-f]{32}$/i.test(taskId);
506
+ if (!isObjectId) {
507
+ const r = await ctx.request.post("https://www.tracup.com/api2/issue/info", {
508
+ multipart: { project: project.id, id: taskId },
509
+ });
510
+ const body = await r.text();
511
+ let parsed;
512
+ try {
513
+ parsed = JSON.parse(body);
514
+ } catch {
515
+ throw new Error(`按编号查询失败(HTTP ${r.status()}):${body.slice(0, 200)}`);
516
+ }
517
+ if (parsed.code !== undefined && parsed.code !== 0) {
518
+ throw new Error(`按编号 ${taskId} 查询失败:${parsed.message || body.slice(0, 200)}`);
519
+ }
520
+ const hit = parsed.data || parsed;
521
+ if (!hit || !hit.id) throw new Error(`按编号 ${taskId} 没查到任务,请改用任务 id(32 位 hex)`);
522
+ taskId = hit.id;
523
+ log(`按编号解析:${project.name} #${hit.number ?? taskId} ${hit.title || ""} → id=${taskId}`);
524
+ }
525
+
526
+ if (!args.yes) {
527
+ log(`将要删除:${project.name} 中 id=${taskId}`);
528
+ log("确认无误后加 --yes 重跑。");
529
+ await ctx.close();
530
+ return;
531
+ }
532
+
533
+ const resp = await ctx.request.post("https://www.tracup.com/api2/issue/delete", {
534
+ multipart: { project: project.id, id: taskId },
535
+ });
536
+ const text = await resp.text();
537
+ await ctx.close();
538
+ let data;
539
+ try {
540
+ data = JSON.parse(text);
541
+ } catch {
542
+ throw new Error(`删除返回非 JSON(HTTP ${resp.status()}):${text.slice(0, 200)}`);
543
+ }
544
+ if (data.code !== undefined && data.code !== 0) {
545
+ throw new Error(`删除失败:${data.message || text.slice(0, 200)}`);
546
+ }
547
+ log(`✓ 已删除:${project.name} id=${taskId}`);
548
+ }
549
+
550
+ function parseArgs(argv) {
551
+ const args = { _: [] };
552
+ for (let i = 0; i < argv.length; i++) {
553
+ const a = argv[i];
554
+ if (a.startsWith("--")) {
555
+ const key = a.slice(2);
556
+ const next = argv[i + 1];
557
+ if (next && !next.startsWith("--")) {
558
+ args[key] = next;
559
+ i++;
560
+ } else args[key] = true;
561
+ } else args._.push(a);
562
+ }
563
+ return args;
564
+ }
565
+
566
+ const args = parseArgs(process.argv.slice(2));
567
+ const cmd = args._[0];
568
+
569
+ const commands = {
570
+ login: cmdLogin,
571
+ status: cmdStatus,
572
+ capture: cmdCapture,
573
+ api: cmdApi,
574
+ projects: cmdProjects,
575
+ create: cmdCreate,
576
+ delete: cmdDelete,
577
+ };
578
+
579
+ if (!cmd || !commands[cmd]) {
580
+ log("用法:node tracup_web.mjs <login|status|projects|create|delete|capture|api>");
581
+ log("");
582
+ log("create 参数:--title(必填)--body --project --priority --type --module --assignee --status --dry-run --json");
583
+ log("优先级:0=无 1=高 2=中 3=低(也可直接写 无/高/中/低,Tracup 只有这三档)");
584
+ process.exit(1);
585
+ }
586
+
587
+ commands[cmd](args).catch((err) => {
588
+ console.error("[error]", err?.message || err);
589
+ process.exit(1);
590
+ });
package/package.json CHANGED
@@ -1,6 +1,33 @@
1
1
  {
2
2
  "name": "tracup-bug",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Tracup 缺陷提单 CLI(免费版无开放 API,走浏览器登录态调网页同款内部接口)",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "tracup": "./bin/tracup_web.mjs",
9
+ "tracup-setup": "./bin/setup.mjs"
10
+ },
11
+ "dependencies": {
12
+ "playwright-core": "^1.49.0"
13
+ },
14
+ "files": [
15
+ "bin/",
16
+ "SKILL.md",
17
+ "README.md"
18
+ ],
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "keywords": [
23
+ "tracup",
24
+ "bug",
25
+ "issue",
26
+ "cli"
27
+ ],
28
+ "homepage": "https://www.tracup.com",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": ""
32
+ }
33
+ }