@getnote/cli 1.5.1 → 1.5.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/README.md CHANGED
@@ -93,6 +93,14 @@ getnote notes
93
93
 
94
94
  ## 检查和升级
95
95
 
96
+ `doctor` 会同时检查 CLI 路径、npm 安装一致性、账号授权、OpenAPI 连通性、版本更新和已检测 AI 平台的 Skill 状态:
97
+
98
+ ```bash
99
+ getnote doctor -o json
100
+ ```
101
+
102
+ 机器或 AI 应同时读取 `ready` 和 `status`:`ready=true,status=ready` 表示完整可用;`ready=true,status=degraded` 表示核心 API 可用但仍有非阻断问题;`ready=false` 表示存在阻断问题。`issues` 提供稳定问题码、严重级别和是否阻断,`next_actions` 给出修复动作与是否需要用户确认;`integrations` 区分“检测到 AI 应用”和“对应 Skill 已安装”。`--offline` 只检查本地环境,此时 `local_ready` 表示本地结果、`ready=null,status=partial` 表示远端状态未知。默认只返回检测到的平台,需要排查全部平台时加 `--all-platforms`。
103
+
96
104
  ```bash
97
105
  # 查看当前版本并检查是否有新版
98
106
  getnote version --check-update
@@ -104,6 +112,8 @@ getnote update --check
104
112
  getnote update
105
113
  ```
106
114
 
115
+ 独立二进制升级会校验 GitHub Release 的 SHA-256;npm 安装会升级整个 `@getnote/cli` 包,使二进制、元数据和内置 Skills 保持一致。
116
+
107
117
  如果通过 npm 安装,也可以完整更新 npm 包和可执行程序:
108
118
 
109
119
  ```bash
@@ -387,6 +397,8 @@ $ getnote search "工作日志" --limit 7 -o json
387
397
 
388
398
  | 日期 | 版本 | 新能力 | 适合怎么用 |
389
399
  |------|------|--------|------------|
400
+ | 2026-08-17 | **v1.5.3** | 1. `doctor` 新增稳定问题码、阻断级别、修复动作、平台 Skill 状态和在线/离线就绪语义<br>2. 诊断失败提供可靠退出码,JSON 保持单一、可解析<br>3. npm 与独立二进制采用匹配的升级路径,Release 下载增加 SHA-256 校验<br>4. WorkBuddy Skills 整套预检并支持失败回滚<br>5. 严格校验输出格式与多余参数 | 让技术用户和 AI 都能准确判断环境是否真正可用,并根据结构化结果完成修复;同时避免版本分裂、半安装和未校验更新包 |
401
+ | 2026-08-14 | **v1.5.2** | 1. 补齐团队知识库、订阅知识库与文件夹操作的执行边界<br>2. 明确目录、资源及笔记 ID 的结构化结果字段,避免 AI 猜字段<br>3. 搜索结果按字符串 `note_id` 去重,不把同一笔记的多个命中片段重复展示<br>4. 完善“这条笔记”、录音分享与权限失败的安全处理 | 提升 CLI 内置 Skills 在知识库整理、搜索和笔记管理场景中的理解与执行准确度;本次不改变既有 CLI 命令和授权凭证 |
390
402
  | 2026-08-13 | **v1.5.1** | 1. 修复通过 `npx setup` 安装后命令指向临时 npm 缓存的问题<br>2. 同时提供稳定的 `getnote` / `gnote` 双命令<br>3. 自动把五个领域 Skill 安装到 WorkBuddy<br>4. 重复安装保留已有授权凭证 | 在 WorkBuddy、Codex、Claude Code 或 Cursor 中重复执行同一条安装命令即可修复或升级,不需要重新授权 |
391
403
  | 2026-08-13 | **v1.5.0** | 1. 支持浏览、创建、移动和删除知识库目录<br>2. 笔记可直接加入指定目录,支持已有团队知识库 `TEAMSPACE` 的成员读取与管理员维护<br>3. 支持订阅抖音博主并读取博主内容<br>4. 新增录音原文、链接原文、附件、时间线、快捷笔记和会议待办等一级命令<br>5. 完善结构化结果、错误原因、短命令、`doctor` 和升级检查 | 让人和 AI 用同一套命令完成知识库整理、资料归档和深层笔记读取;失败时能识别原因,不再猜参数或把未完成任务当成成功 |
392
404
  | 2026-08-11 | **v1.4.0** | 1. Skill 2.0 稳定执行契约<br>2. 所有子命令完整 `-h` / `--help`<br>3. `getnote update --check` 与升级引导<br>4. `--content-file` / `--stdin` 安全保存长文本 | 让独立 Skill 和 CLI 内置原子 Skills 共用同一套真实命令,降低 AI 猜参数和长内容转义失败的概率 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getnote/cli",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
4
4
  "description": "CLI tool for 得到大脑(Get笔记) — manage notes and knowledge bases from the terminal",
5
5
  "keywords": [
6
6
  "getnote",
@@ -6,7 +6,9 @@
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
  const os = require('os');
9
- const { execSync } = require('child_process');
9
+ const https = require('https');
10
+ const crypto = require('crypto');
11
+ const { spawnSync } = require('child_process');
10
12
 
11
13
  const pkg = require('../package.json');
12
14
  const VERSION = pkg.version;
@@ -46,7 +48,9 @@ async function main() {
46
48
  // Skip download if binary already matches current version
47
49
  if (fs.existsSync(binaryPath)) {
48
50
  try {
49
- const out = execSync(`"${binaryPath}" version 2>/dev/null || "${binaryPath}" --version 2>/dev/null`, { encoding: 'utf8' }).trim();
51
+ const result = spawnSync(binaryPath, ['version'], { encoding: 'utf8' });
52
+ const fallback = result.status === 0 ? result : spawnSync(binaryPath, ['--version'], { encoding: 'utf8' });
53
+ const out = (fallback.stdout || '').trim();
50
54
  if (out.includes(VERSION)) {
51
55
  console.log(`getnote v${VERSION} already installed, skipping download.`);
52
56
  return;
@@ -60,16 +64,12 @@ async function main() {
60
64
  fs.mkdirSync(binDir, { recursive: true });
61
65
 
62
66
  try {
63
- // Use curl (available on macOS/Linux) or PowerShell (Windows) for reliable redirect handling
67
+ await download(url, tmpFile);
68
+ await verifyChecksum(url, path.basename(url), tmpFile);
64
69
  if (platform.platform === 'windows') {
65
- execSync(
66
- `powershell -Command "Invoke-WebRequest -Uri '${url}' -OutFile '${tmpFile}'"`,
67
- { stdio: 'inherit' }
68
- );
69
- execSync(`powershell -Command "Expand-Archive -Path '${tmpFile}' -DestinationPath '${binDir}' -Force"`, { stdio: 'inherit' });
70
+ run('powershell', ['-NoProfile', '-Command', 'Expand-Archive -LiteralPath $args[0] -DestinationPath $args[1] -Force', tmpFile, binDir]);
70
71
  } else {
71
- execSync(`curl -fsSL "${url}" -o "${tmpFile}"`, { stdio: 'inherit' });
72
- execSync(`tar -xzf "${tmpFile}" -C "${binDir}" "${binaryName}"`, { stdio: 'inherit' });
72
+ run('tar', ['-xzf', tmpFile, '-C', binDir, binaryName]);
73
73
  }
74
74
 
75
75
  fs.chmodSync(binaryPath, 0o755);
@@ -83,4 +83,52 @@ async function main() {
83
83
  }
84
84
  }
85
85
 
86
- main();
86
+ function run(command, args) {
87
+ const result = spawnSync(command, args, { stdio: 'inherit' });
88
+ if (result.error) throw result.error;
89
+ if (result.status !== 0) throw new Error(`${command} exited with status ${result.status}`);
90
+ }
91
+
92
+ function download(url, destination, redirects = 0) {
93
+ if (redirects > 5) return Promise.reject(new Error('Too many redirects'));
94
+ return new Promise((resolve, reject) => {
95
+ const request = https.get(url, { headers: { 'User-Agent': '@getnote/cli installer' } }, response => {
96
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
97
+ response.resume();
98
+ return resolve(download(new URL(response.headers.location, url).toString(), destination, redirects + 1));
99
+ }
100
+ if (response.statusCode !== 200) {
101
+ response.resume();
102
+ return reject(new Error(`HTTP ${response.statusCode}: ${url}`));
103
+ }
104
+ const output = fs.createWriteStream(destination, { mode: 0o600 });
105
+ response.pipe(output);
106
+ output.on('finish', () => output.close(resolve));
107
+ output.on('error', reject);
108
+ });
109
+ request.on('error', reject);
110
+ });
111
+ }
112
+
113
+ async function verifyChecksum(assetURL, assetName, archivePath) {
114
+ const checksumPath = `${archivePath}.checksums`;
115
+ try {
116
+ await download(new URL('checksums.txt', assetURL).toString(), checksumPath);
117
+ const line = fs.readFileSync(checksumPath, 'utf8').split(/\r?\n/).find(value => {
118
+ const fields = value.trim().split(/\s+/);
119
+ return fields.length === 2 && fields[1].replace(/^\*/, '') === assetName;
120
+ });
121
+ if (!line) throw new Error(`Checksum for ${assetName} is missing`);
122
+ const expected = line.trim().split(/\s+/)[0].toLowerCase();
123
+ if (!/^[a-f0-9]{64}$/.test(expected)) throw new Error(`Checksum for ${assetName} is invalid`);
124
+ const actual = crypto.createHash('sha256').update(fs.readFileSync(archivePath)).digest('hex');
125
+ if (actual !== expected) throw new Error(`Checksum mismatch for ${assetName}`);
126
+ } finally {
127
+ try { fs.unlinkSync(checksumPath); } catch (_) {}
128
+ }
129
+ }
130
+
131
+ main().catch(err => {
132
+ console.error('Failed to install getnote:', err.message);
133
+ process.exit(1);
134
+ });
@@ -15,7 +15,7 @@ description: 安装和连接得到大脑,完成浏览器授权、环境诊断
15
15
  2. 若当前环境只有 CLI 内置 Skill、没有上述安装器,则用 `command -v getnote` 检查 CLI;缺失时检查 `node --version` 和 `npm --version`,再自动执行 `npm install -g @getnote/cli@latest`。这是 Agent 的工作,不要求用户手工复制命令;只有系统弹出安装授权时才请用户确认。
16
16
  3. 执行 `getnote version`,必须能够正常启动。
17
17
  4. 执行 `getnote auth status`。未登录时运行 `getnote auth login`,让用户只在浏览器中确认,不索要 API Key、Cookie 或 Authorization。
18
- 5. 执行 `getnote doctor -o json`。只有 `success=true`,且 `checks` 中 `cli`、`auth`、`api` 均为 `ok=true`,才能宣布连接完成。
18
+ 5. 执行 `getnote doctor -o json`。只有 `diagnostics_completed=true`、`ready=true` 且 `status=ready`,才能宣布完整连接;`ready=true,status=degraded` 表示核心能力可用但仍应处理警告。`success` 和旧 `checks` 字段仅用于兼容。若未就绪,先处理 `issues[].blocking=true`,再按 `next_actions[]` 修复。需要确认的动作不得静默执行。
19
19
  6. 在 Codex、Claude Code 或 Cursor 等本地 Agent 中,执行 `getnote setup` 同步 5 个领域 Skill;如果当前平台已经由独立 Skill 包携带这些领域 Skill,或 CLI 明确提示未检测到受支持平台,不把这一步失败误报成账号连接失败。
20
20
  7. 先用 `getnote notes --limit 1 -o json` 做无写入验收。只有用户同意创建测试内容时,才保存测试笔记。
21
21
 
@@ -43,7 +43,7 @@ description: 安装和连接得到大脑,完成浏览器授权、环境诊断
43
43
  | `getnote auth login` | 浏览器已确认,凭证已写入本机 | 再运行 `doctor -o json`;不在聊天中展示凭证。 |
44
44
  | `getnote auth status` | `Authenticated` / `Not authenticated` 或环境变量登录状态 | 未登录才启动 `auth login`;状态里只能出现掩码。 |
45
45
  | `getnote auth logout` | `Logged out successfully.` | 只说明本机已退出;不声称已撤销服务端授权。 |
46
- | `getnote doctor -o json` | `success`、`cli_version`、`checks[].name/ok/message`、`platforms[]` | 只有 `cli`、`auth`、`api` 均为 `ok=true` 才宣布可用。 |
46
+ | `getnote doctor -o json` | `diagnostics_completed`、`local_ready`、`ready/status/summary`、`checks[]`、`issues[]`、`next_actions[]`、`update`、`integrations[]` | `ready=true,status=ready` 才表示完整可用;`degraded` 需继续处理警告。`platforms[]` 仅表示检测到应用或命令,不能证明 Skill 已安装;用 `integrations[].skill_status/ready` 判断 AI 接入状态。 |
47
47
  | `getnote capabilities -o json` | `contract_version`、`commands`、`command_aliases`、`command_results`、`guarantees` | 只在安装、升级或兼容排查时读取;这是命令和结果字段的唯一事实源。 |
48
48
  | `getnote setup -o json` | `success`、`targets[]`、`installed_skills`、`authenticated`、`next` | 仅同步本机 Agent 的领域 Skill;没有可识别目标不等于账号失败。 |
49
49
  | `getnote quota -o json` | `data.read/write/write_note` 下的 `daily/monthly.limit/used/remaining/reset_at` | 按真实桶说明剩余额度,不自行换算或合并桶。 |
@@ -53,6 +53,16 @@ description: 安装和连接得到大脑,完成浏览器授权、环境诊断
53
53
 
54
54
  所有命令以退出码为第一判断:退出码非 0 即失败。使用 `-o json` 的 API 与本地错误均返回 `success=false`、`data=null`、`error.code/message/reason/retryable` 和可选 `request_id`;不能把 HTTP 200 或“命令运行过”当成成功。
55
55
 
56
+ ### Doctor 机器决策规则
57
+
58
+ 1. `diagnostics_completed=false` 或命令退出非 0:诊断本身失败,不能根据残缺输出操作。
59
+ 2. `ready=true`:CLI、账号授权和 OpenAPI 连通性均可用;若 `status=degraded`,核心能力可用但仍有非阻断警告。
60
+ 3. `status=partial`:通常是 `--offline` 跳过远端检查;读取 `local_ready` 判断本地环境,`ready=null` 表示远端未知,不得声称已连接。
61
+ 4. `issues[].blocking=true`:按顺序优先处理;保留 `code`、`details.request_id` 和错误字段。
62
+ 5. `next_actions[]`:`requires_confirmation=true` 时先向用户确认,再执行精确 `command`;执行后重新运行 doctor 验证。
63
+ 6. `integrations[].detected=true` 只表示发现宿主;只有 `skill_status=installed` 且 `ready=true` 才能确认 CLI 可验证的 Skill 已齐全。`unverified` 表示安装状态由平台管理,应让用户在平台内确认,不能猜测成功。
64
+ 7. `update.update_available=true` 是非阻断警告;用户明确要求升级后才执行升级动作。
65
+
56
66
  ## 更新闭环
57
67
 
58
68
  用户说“帮我更新得到大脑”已经构成更新授权:
@@ -14,7 +14,7 @@ description: 查看和管理得到大脑的默认知识库、书籍、客户档
14
14
  | 意图 | 命令入口 |
15
15
  |---|---|
16
16
  | 自有/可管理知识库 | `getnote kbs` |
17
- | 订阅知识库 | `getnote kbs-sub` |
17
+ | 我订阅的知识库 | `getnote kbs-sub` |
18
18
  | 知识库笔记 | `getnote kb <topic_id>` |
19
19
  | 新建个人知识库 | `getnote kb create` |
20
20
  | 加入笔记 | `getnote kb add` |
@@ -35,15 +35,16 @@ description: 查看和管理得到大脑的默认知识库、书籍、客户档
35
35
 
36
36
  ## 选择知识库和权限
37
37
 
38
- 1. 先执行 `getnote kbs -o json`,保留全部真实 Scope:`DEFAULT`、`BOOKSPACE`、`CUSTOMER`、`TEAMSPACE`,不能只返回默认知识库。
39
- 2. 按名称和 `scope` 匹配;同名或用户意图不明确时让用户选择,不猜 `topic_id`。
40
- 3. 订阅知识库通常只读。团队知识库(`TEAMSPACE`)成员可以读取;只有接口返回具备维护权限的拥有者或管理员才能创建目录、加入笔记或订阅内容。
41
- 4. 普通成员写入失败时明确说明权限不足,不尝试绕过。当前不代替用户新建团队知识库。
38
+ 1. 用户问“我管理的/我的知识库”时执行 `getnote kbs -o json`;问“所有能访问的知识库”时还要执行 `getnote kbs-sub -o json`,分别标明自有/可管理与已订阅,按各自 `has_more` 翻完页,不能混成同一种权限。
39
+ 2. `getnote kbs` 必须保留接口实际返回的全部 Scope,例如 `DEFAULT`、`BOOKSPACE`、`CUSTOMER`、`TEAMSPACE`,不能只返回默认知识库,也不能虚构未返回的 Scope。
40
+ 3. 按名称和 `scope` 匹配;同名或用户意图不明确时让用户选择,不猜 `topic_id`。目标名称既可能是知识库也可能是文件夹时,先问清楚。
41
+ 4. 订阅知识库通常只读。若列表结果明确返回角色或可写标记,按真实字段判断;当前 CLI 契约未保证权限字段时,不得声称已经预检为 owner/admin,可在用户明确授权后尝试写入,并忠实处理权限失败。
42
+ 5. 普通成员写入失败时明确说明权限不足,不尝试绕过。当前不代替用户新建团队知识库。
42
43
 
43
44
  ## 文件夹和归档流程
44
45
 
45
46
  1. 用户要求放入文件夹时,用 `gnote kb dir <topic_id> -o json` 读取根目录或指定目录。
46
- 2. 已有文件夹使用返回的真实 `directory_id`;缺失时先询问是否创建,再用 `mkdir`。
47
+ 2. 已有文件夹使用 `data.directories[].id` 返回的真实目录 ID;浏览指定目录时把该值传给 `--directory-id`。缺失时先询问是否创建,再用 `mkdir`。
47
48
  3. `getnote kb add` 同时传真实 `topic_id`、字符串 `note_id` 和 CLI 帮助中规定的目录参数。
48
49
  4. 每批最多 20 条。移出笔记和删除目录必须先确认;删除目录还必须由 CLI/服务校验为空。
49
50
  5. 移动或重命名时只改变用户指定项,未指定的名称或父目录保持不变。
@@ -64,7 +65,7 @@ description: 查看和管理得到大脑的默认知识库、书籍、客户档
64
65
  | `getnote kb create <name> -o json` | `success=true`、`data?` | 仅创建个人知识库;不得在没有返回 ID 时虚构 `topic_id`。 |
65
66
  | `getnote kb add <topic_id> <note_id…> -o json` | `success=true`、`data?` | 最多 20 条;需要确认最终目录归属时重新读取目录。 |
66
67
  | `getnote kb remove <topic_id> <note_id…> --yes -o json` | `success=true`、`data?` | 最多 20 条;先确认,再说明已从该知识库移出。 |
67
- | `gnote kb dir <topic_id> -o json` | `success=true`、`data.current_directory?`、`directories[].id/name`、`resources[]`、`total` | 只使用返回的 `directory_id`;旧环境回退 `getnote kb directories`。 |
68
+ | `gnote kb dir <topic_id> -o json` | `success=true`、`data.current_directory?`、`directories[].id/name`、`resources[].id/directory_id/note_id?/name/type/status`、`total` | 目录主键读取 `directories[].id`;资源归属读取 `resources[].directory_id`,笔记资源用 `resources[].note_id` 验证;旧环境回退 `getnote kb directories`。 |
68
69
  | `gnote kb mkdir <topic_id> --name <name> -o json` | `success=true`、`data?` | 也兼容位置参数 `<name>`;二者不可同时使用。若需给出新目录 ID 或层级,随后重新读取目录。 |
69
70
  | `gnote kb mvdir <topic_id> <directory_id> … -o json` | `success=true`、`data?` | 只确认用户指定的改名/移动;需要最终名称或父级时重新读取目录。 |
70
71
  | `gnote kb rmdir <topic_id> <directory_id> --yes -o json` | `success=true`、`data?` | 只删除空目录;说明已删除前必须拿到业务成功。 |
@@ -82,6 +83,7 @@ description: 查看和管理得到大脑的默认知识库、书籍、客户档
82
83
 
83
84
  - `kbs`、`kbs-sub`、`kb`、`dir`、博主和直播的空列表都是成功结果:如实说“目前没有”,不要自动创建知识库、目录或订阅。
84
85
  - `kb create`、`kb add`、`mkdir`、`mvdir` 等写操作的 `data` 可能因服务版本不同而不提供完整对象。只在 `success=true` 时确认动作已提交;若要向用户返回新目录 ID、最终层级或笔记归属,必须紧接着重新读取 `gnote kb dir` 或 `getnote kb <topic_id>`,不能猜字段。
85
- - `kb add` 即使请求成功,也不能把“已发起加入”说成“已进入某个目录”;只有重新读取后出现对应 `note_id` / `directory_id` 才能展示最终归属。
86
+ - `kb add` 即使请求成功,也不能把“已发起加入”说成“已进入某个目录”;只有重新读取后在 `resources[]` 的同一项出现对应字符串 `note_id` 和 `directory_id`,才能展示最终归属。
86
87
  - `kb remove`、`rmdir` 前必须有用户明确确认并带 `--yes`;若 `reason=knowledge_directory_not_empty`,明确提示“目录非空,请先移出内容或删除子目录”,不要原请求重试。其它失败也应说明真实原因和下一步,绝不宣称已删除。
87
88
  - 用户给的团队知识库不在 `getnote kbs` 返回中时,先说明当前账号没有访问权限;不能把个人知识库同名项替代成团队知识库。
89
+ - “这条笔记”只能复用当前会话中已经由 CLI 返回并验证过的字符串 `note_id`。没有可靠上下文时先请用户提供笔记 ID;不能从不受支持的私有链接、标题或雪花 ID 数值转换中猜测。
@@ -71,7 +71,7 @@ description: 使用得到大脑保存文字、链接、图片和长笔记,查
71
71
  ## 查询和深层读取
72
72
 
73
73
  1. “最近、列表、有哪些”使用 `getnote notes`;“找、搜、关于某主题”交给搜索 Skill。
74
- 2. 用户给出 ID 或私有笔记链接时读取详情;雪花 ID 全程按字符串原样传递。
74
+ 2. 用户给出 ID 时直接读取详情;雪花 ID 全程按字符串原样传递。“这条笔记”只复用当前会话中已经由 CLI 返回并验证过的字符串 ID。当前 CLI 若不能直接接收某种私有链接,就先请用户提供 ID,不能自行截取、猜测或转成数字。
75
75
  3. 列表先展示标题、字符串 ID 和真实 `note_url`,用户选择后再读取全文。
76
76
  4. 不确定笔记类型时先读 `getnote note <id> -o json`:
77
77
  - 链接/文字原文:`original`;
@@ -87,7 +87,8 @@ description: 使用得到大脑保存文字、链接、图片和长笔记,查
87
87
  1. 先读取目标笔记和当前版本,确认用户指向的对象。
88
88
  2. 追加或前置内容必须使用 CLI 当前帮助中对应的增量语义,不用覆盖模拟追加。
89
89
  3. 覆盖正文、替换全部标签、删除和公开分享必须先确认;确认后才使用 `--yes`。
90
- 4. 用户未要求公开时只返回私有 `note_url`,不自动生成分享链接。
90
+ 4. 分享录音类笔记时,确认话术必须说明公开链接是否包含音频;用户不希望公开音频时使用 CLI 帮助中的 `--exclude-audio`。不能替用户默认决定音频公开范围。
91
+ 5. 用户未要求公开时只返回私有 `note_url`,不自动生成分享链接。
91
92
 
92
93
  ## 每条命令的结果与回复格式
93
94
 
@@ -19,6 +19,7 @@ description: 在得到大脑的全部笔记或指定知识库中按自然语言
19
19
  2. 用户指定知识库名称时,先执行 `getnote kbs -o json`,用返回的真实 `topic_id` 和 `scope` 匹配;同名、多个团队知识库或意图不明确时必须让用户选择。
20
20
  3. 已确认知识库后执行 `getnote search <query> --kb <topic_id> --limit <1-10> -o json`。
21
21
  4. 只从返回值读取标题、摘要、字符串 `note_id` 和真实 `note_url`。没有 `note_id/note_url` 的非笔记结果可以展示内容,但不能伪造“打开笔记”链接。
22
+ 5. 同一篇笔记可能因命中多个片段出现多次。面向用户列“几篇笔记”时必须按字符串 `note_id` 去重;若去重后不足用户要求的数量,可在上限 10 内增大 `--limit` 重搜,仍不足时如实返回实际数量。
22
23
 
23
24
  ## 命令结果与用户呈现
24
25