best-geo-test 0.1.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.
@@ -0,0 +1,362 @@
1
+ {
2
+ "protocolVersion": 1,
3
+ "capabilities": [
4
+ {
5
+ "name": "companies.list",
6
+ "mode": "read",
7
+ "guide": "company",
8
+ "requiresPlan": false,
9
+ "description": "列出当前密钥被授权的公司。不传 companyId 就是全部授权公司。",
10
+ "inputSchema": {
11
+ "type": "object",
12
+ "properties": {
13
+ "id": {
14
+ "type": "number",
15
+ "description": "只看某一家公司"
16
+ },
17
+ "name": {
18
+ "type": "string",
19
+ "description": "按名称模糊匹配"
20
+ },
21
+ "search": {
22
+ "type": "string",
23
+ "description": "同时按名称和 ID 搜索"
24
+ },
25
+ "page": {
26
+ "type": "number",
27
+ "default": 1
28
+ },
29
+ "limit": {
30
+ "type": "number",
31
+ "default": 20,
32
+ "maximum": 100
33
+ }
34
+ },
35
+ "additionalProperties": false
36
+ }
37
+ },
38
+ {
39
+ "name": "companies.get",
40
+ "mode": "read",
41
+ "guide": "company",
42
+ "requiresPlan": false,
43
+ "description": "按 ID 读取一家公司。ID 必须在授权范围内。",
44
+ "inputSchema": {
45
+ "type": "object",
46
+ "properties": {
47
+ "id": {
48
+ "type": "number"
49
+ }
50
+ },
51
+ "required": [
52
+ "id"
53
+ ],
54
+ "additionalProperties": false
55
+ }
56
+ },
57
+ {
58
+ "name": "companies.create",
59
+ "mode": "write",
60
+ "guide": "company",
61
+ "requiresPlan": true,
62
+ "description": "新建公司。创建成功后这家公司会自动加入当前密钥的授权范围。",
63
+ "inputSchema": {
64
+ "type": "object",
65
+ "properties": {
66
+ "name": {
67
+ "type": "string",
68
+ "description": "公司名称"
69
+ },
70
+ "description": {
71
+ "type": "string",
72
+ "description": "公司描述"
73
+ },
74
+ "aliases": {
75
+ "type": "array",
76
+ "items": {
77
+ "type": "string"
78
+ },
79
+ "description": "公司别名/关键词"
80
+ }
81
+ },
82
+ "required": [
83
+ "name",
84
+ "description"
85
+ ],
86
+ "additionalProperties": false
87
+ }
88
+ },
89
+ {
90
+ "name": "companies.update",
91
+ "mode": "write",
92
+ "guide": "company",
93
+ "requiresPlan": true,
94
+ "description": "修改公司。只改传进来的字段,没传的保持不变;数组字段是整体覆盖。",
95
+ "inputSchema": {
96
+ "type": "object",
97
+ "properties": {
98
+ "companyId": {
99
+ "type": "number"
100
+ },
101
+ "name": {
102
+ "type": "string"
103
+ },
104
+ "description": {
105
+ "type": "string"
106
+ },
107
+ "aliases": {
108
+ "type": "array",
109
+ "items": {
110
+ "type": "string"
111
+ },
112
+ "description": "整体覆盖,不是追加"
113
+ }
114
+ },
115
+ "required": [
116
+ "companyId"
117
+ ],
118
+ "additionalProperties": false
119
+ }
120
+ },
121
+ {
122
+ "name": "products.list",
123
+ "mode": "read",
124
+ "guide": "product",
125
+ "requiresPlan": false,
126
+ "description": "列出 GEO 项目。传 companyId 可限定到某家公司。",
127
+ "inputSchema": {
128
+ "type": "object",
129
+ "properties": {
130
+ "companyId": {
131
+ "type": "number"
132
+ },
133
+ "name": {
134
+ "type": "string"
135
+ },
136
+ "search": {
137
+ "type": "string"
138
+ },
139
+ "page": {
140
+ "type": "number",
141
+ "default": 1
142
+ },
143
+ "limit": {
144
+ "type": "number",
145
+ "default": 20,
146
+ "maximum": 100
147
+ }
148
+ },
149
+ "additionalProperties": false
150
+ }
151
+ },
152
+ {
153
+ "name": "products.get",
154
+ "mode": "read",
155
+ "guide": "product",
156
+ "requiresPlan": false,
157
+ "description": "按 ID 读取一个 GEO 项目。",
158
+ "inputSchema": {
159
+ "type": "object",
160
+ "properties": {
161
+ "id": {
162
+ "type": "number"
163
+ }
164
+ },
165
+ "required": [
166
+ "id"
167
+ ],
168
+ "additionalProperties": false
169
+ }
170
+ },
171
+ {
172
+ "name": "products.create",
173
+ "mode": "write",
174
+ "guide": "product",
175
+ "requiresPlan": true,
176
+ "description": "在指定公司下新建 GEO 项目。",
177
+ "inputSchema": {
178
+ "type": "object",
179
+ "properties": {
180
+ "companyId": {
181
+ "type": "number"
182
+ },
183
+ "name": {
184
+ "type": "string"
185
+ },
186
+ "keyword": {
187
+ "type": "array",
188
+ "items": {
189
+ "type": "string"
190
+ },
191
+ "description": "关键词"
192
+ },
193
+ "targetWord": {
194
+ "type": "array",
195
+ "items": {
196
+ "type": "string"
197
+ },
198
+ "description": "目标词"
199
+ }
200
+ },
201
+ "required": [
202
+ "companyId",
203
+ "name",
204
+ "keyword",
205
+ "targetWord"
206
+ ],
207
+ "additionalProperties": false
208
+ }
209
+ },
210
+ {
211
+ "name": "products.update",
212
+ "mode": "write",
213
+ "guide": "product",
214
+ "requiresPlan": true,
215
+ "description": "修改 GEO 项目。只改传进来的字段;数组字段是整体覆盖。",
216
+ "inputSchema": {
217
+ "type": "object",
218
+ "properties": {
219
+ "productId": {
220
+ "type": "number"
221
+ },
222
+ "name": {
223
+ "type": "string"
224
+ },
225
+ "keyword": {
226
+ "type": "array",
227
+ "items": {
228
+ "type": "string"
229
+ },
230
+ "description": "整体覆盖,不是追加"
231
+ },
232
+ "targetWord": {
233
+ "type": "array",
234
+ "items": {
235
+ "type": "string"
236
+ },
237
+ "description": "整体覆盖,不是追加"
238
+ }
239
+ },
240
+ "required": [
241
+ "productId"
242
+ ],
243
+ "additionalProperties": false
244
+ }
245
+ },
246
+ {
247
+ "name": "questions.list",
248
+ "mode": "read",
249
+ "guide": "question",
250
+ "requiresPlan": false,
251
+ "description": "列出搜索问题。可按公司或 GEO 项目限定。",
252
+ "inputSchema": {
253
+ "type": "object",
254
+ "properties": {
255
+ "companyId": {
256
+ "type": "number"
257
+ },
258
+ "productId": {
259
+ "type": "number"
260
+ },
261
+ "topic": {
262
+ "type": "string",
263
+ "description": "按问题文本模糊匹配"
264
+ },
265
+ "page": {
266
+ "type": "number",
267
+ "default": 1
268
+ },
269
+ "limit": {
270
+ "type": "number",
271
+ "default": 20,
272
+ "maximum": 100
273
+ }
274
+ },
275
+ "additionalProperties": false
276
+ }
277
+ },
278
+ {
279
+ "name": "questions.get",
280
+ "mode": "read",
281
+ "guide": "question",
282
+ "requiresPlan": false,
283
+ "description": "按 ID 读取一个搜索问题。",
284
+ "inputSchema": {
285
+ "type": "object",
286
+ "properties": {
287
+ "id": {
288
+ "type": "number"
289
+ }
290
+ },
291
+ "required": [
292
+ "id"
293
+ ],
294
+ "additionalProperties": false
295
+ }
296
+ },
297
+ {
298
+ "name": "questions.create",
299
+ "mode": "write",
300
+ "guide": "question",
301
+ "requiresPlan": true,
302
+ "description": "给 GEO 项目批量新增搜索问题。一次最多 100 条;已存在的、重复的和空值会被跳过,计划摘要里会分别列出。",
303
+ "inputSchema": {
304
+ "type": "object",
305
+ "properties": {
306
+ "productId": {
307
+ "type": "number"
308
+ },
309
+ "questions": {
310
+ "type": "array",
311
+ "items": {
312
+ "type": "string"
313
+ },
314
+ "maxItems": 100,
315
+ "description": "问题文本数组,不是换行字符串"
316
+ },
317
+ "tags": {
318
+ "type": "array",
319
+ "items": {
320
+ "type": "string"
321
+ }
322
+ }
323
+ },
324
+ "required": [
325
+ "productId",
326
+ "questions"
327
+ ],
328
+ "additionalProperties": false
329
+ }
330
+ },
331
+ {
332
+ "name": "questions.update",
333
+ "mode": "write",
334
+ "guide": "question",
335
+ "requiresPlan": true,
336
+ "description": "修改一条搜索问题。",
337
+ "inputSchema": {
338
+ "type": "object",
339
+ "properties": {
340
+ "questionId": {
341
+ "type": "number"
342
+ },
343
+ "topic": {
344
+ "type": "string",
345
+ "description": "问题文本"
346
+ },
347
+ "tags": {
348
+ "type": "array",
349
+ "items": {
350
+ "type": "string"
351
+ },
352
+ "description": "整体覆盖,不是追加"
353
+ }
354
+ },
355
+ "required": [
356
+ "questionId"
357
+ ],
358
+ "additionalProperties": false
359
+ }
360
+ }
361
+ ]
362
+ }
package/dist/api.js ADDED
@@ -0,0 +1,111 @@
1
+ import { API_BASE, CLI_VERSION, PROTOCOL_VERSION, REQUEST_TIMEOUT_MS } from "./config.js";
2
+ import { peekDeviceId } from "./device.js";
3
+ import { fail } from "./envelope.js";
4
+ /**
5
+ * 服务端调用。
6
+ *
7
+ * 服务端的 /v1/agent-cli/** 已经返回同一套信封(成功和失败都是),
8
+ * 所以这里**原样透传**,不重新包装 —— 中间多一层转换就多一处口径漂移的机会。
9
+ * 只有网络层自身出错时才由 CLI 生成信封。
10
+ */
11
+ export async function callApi(path, options = {}) {
12
+ const url = new URL(`${API_BASE}/v1/agent-cli${path}`);
13
+ for (const [key, value] of Object.entries(options.query ?? {})) {
14
+ if (value == null)
15
+ continue;
16
+ for (const item of Array.isArray(value) ? value : [value]) {
17
+ url.searchParams.append(key, String(item));
18
+ }
19
+ }
20
+ const headers = {
21
+ "X-GEO-CLI-Version": CLI_VERSION,
22
+ "X-GEO-Protocol-Version": String(PROTOCOL_VERSION),
23
+ };
24
+ // 设备 ID 只在已经有的时候带上,绝不在这里顺手创建:
25
+ // 真正需要建它的是 auth login(那条路径显式调 deviceInfo())。
26
+ // 在这里创建会让 version --check 这种无授权调用也去写用户目录
27
+ // 读不出来就不带这个头:version --check 这类无授权命令不该被本机文件状态卡死。
28
+ // 真正需要设备身份的是 auth login,那条路径显式调 deviceInfo(),权限问题会在那里如实报出来
29
+ let deviceId = null;
30
+ try {
31
+ deviceId = peekDeviceId();
32
+ }
33
+ catch {
34
+ /* 本机配置有问题时照常发请求,由需要它的命令去报错 */
35
+ }
36
+ if (deviceId) {
37
+ headers["X-Device-ID"] = deviceId;
38
+ }
39
+ if (options.apiKey) {
40
+ headers.Authorization = `Bearer ${options.apiKey}`;
41
+ }
42
+ if (options.body != null) {
43
+ headers["Content-Type"] = "application/json";
44
+ }
45
+ try {
46
+ const response = await fetch(url, {
47
+ method: options.method ?? "GET",
48
+ headers,
49
+ body: options.body == null ? undefined : JSON.stringify(options.body),
50
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
51
+ });
52
+ const text = await response.text();
53
+ try {
54
+ const parsed = JSON.parse(text);
55
+ // 必须校验形状,不能只 cast。守卫、限流、全局异常过滤器、网关都可能在进我们的模块之前
56
+ // 返回自己的 JSON(`{"statusCode":401,...}`),那是合法 JSON 但不是信封 ——
57
+ // 直接透传等于让 stdout 上出现一个没有 code / nextAction 的对象,模型读到的是 undefined。
58
+ // body 是字面量 null 时更糟:emit 里读 .ok 抛 TypeError,被兜底再 emit 一次,stdout 出现两行
59
+ if (!isEnvelope(parsed)) {
60
+ return fail("SERVER_ERROR", `服务端返回了非预期内容(HTTP ${response.status})`, {
61
+ // 这不是服务端的终局裁决,只是「没拿到裁决」,pending 不能因此被清掉
62
+ data: { transportFailure: true },
63
+ });
64
+ }
65
+ return parsed;
66
+ }
67
+ catch {
68
+ // 服务端没按信封返回(网关错误页、502 之类)。不把原文透出去:
69
+ // 里面可能有内部地址和堆栈,而模型会拿它当特征去匹配
70
+ return fail("SERVER_ERROR", `服务端返回了非预期内容(HTTP ${response.status})`, {
71
+ data: { transportFailure: true },
72
+ });
73
+ }
74
+ }
75
+ catch (error) {
76
+ return fail("SERVER_ERROR", `无法连接服务端:${error instanceof Error ? error.message : String(error)}`, {
77
+ data: { transportFailure: true },
78
+ nextAction: { type: "ask_user", message: "请确认网络可用,以及服务端地址配置正确" },
79
+ });
80
+ }
81
+ }
82
+ /** 是不是一个合法信封。只认 ok 和 code 两个字段 —— 它们是模型判断时真正依赖的东西 */
83
+ function isEnvelope(value) {
84
+ if (typeof value !== "object" || value === null) {
85
+ return false;
86
+ }
87
+ const candidate = value;
88
+ return typeof candidate.ok === "boolean" && typeof candidate.code === "string";
89
+ }
90
+ /**
91
+ * 这次失败是「没拿到服务端裁决」,而不是「服务端裁定失败」。
92
+ *
93
+ * 两者必须分开:撤销 pending、放弃重试这类不可逆动作,只能建立在服务端真的给了结论上。
94
+ * 网络不通时把它当成「服务端拒绝」,会把本机唯一的恢复依据删掉。
95
+ */
96
+ export function isTransportFailure(envelope) {
97
+ return !envelope.ok && envelope.data?.transportFailure === true;
98
+ }
99
+ /**
100
+ * 这次失败是不是**服务端的终局裁决**。
101
+ *
102
+ * 只有终局裁决才能用来做「撤销 pending」这类不可逆动作。
103
+ * SERVER_ERROR / RATE_LIMITED / CLI_UPDATE_REQUIRED 都不是裁决 ——
104
+ * 服务端 500 时,那次绑定到底成没成功谁也不知道,按「拒绝」处理就会把用户刚粘的密钥删掉。
105
+ */
106
+ export function isServerVerdict(envelope) {
107
+ if (envelope.ok || isTransportFailure(envelope)) {
108
+ return false;
109
+ }
110
+ return ["AUTH_EXPIRED", "DEVICE_MISMATCH", "ACCESS_DENIED", "AUTH_REQUIRED", "VALIDATION_FAILED"].includes(envelope.code);
111
+ }
package/dist/args.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 极简参数解析:只认 `--key value` 和 `--flag`。
3
+ *
4
+ * 不引 commander 之类的库 —— 这个包被全局安装在用户机器上,
5
+ * 依赖越少,安装越快、供应链面越小。
6
+ */
7
+ export function parseFlags(argv) {
8
+ const flags = {};
9
+ for (let i = 0; i < argv.length; i += 1) {
10
+ const token = argv[i];
11
+ if (!token.startsWith("--"))
12
+ continue;
13
+ const name = token.slice(2);
14
+ const next = argv[i + 1];
15
+ if (next && !next.startsWith("--")) {
16
+ flags[name] = next;
17
+ i += 1;
18
+ }
19
+ else {
20
+ flags[name] = true;
21
+ }
22
+ }
23
+ return flags;
24
+ }
@@ -0,0 +1,7 @@
1
+ /** 由 scripts/stamp-env.mjs 在构建时生成。不要手改,也不要提交 */
2
+ export const BUILD_ENV = "dev";
3
+ export const BUILD_API_BASE = "https://nbgeo.aimusiclj.com";
4
+ export const BUILD_COMMAND_NAME = "best-geo-test";
5
+ export const BUILD_PACKAGE_NAME = "best-geo-test";
6
+ export const BUILD_DEVICE_PREFIX = "bg-test-cli-";
7
+ export const BUILD_VERSION = "0.1.0";