@yizhuan-cli/cli 0.1.3-beta.3 → 0.1.4

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
@@ -34,23 +34,47 @@ yizhuan config path
34
34
  yizhuan config show
35
35
  yizhuan execute --ability membership
36
36
  yizhuan execute --ability works --query "百家号昨天新增了哪些作品"
37
- yizhuan execute --ability accounts --params "{\"mode\":\"public_search\",\"platform\":\"toutiao\",\"keyword\":\"张三\"}"
38
- yizhuan execute --ability radar --params "{\"mode\":\"following\"}"
39
- yizhuan execute --ability radar --params "{\"mode\":\"timeline\",\"timeRange\":\"7d\"}"
40
- yizhuan execute --ability radar --params "{\"mode\":\"unfollow\",\"articleId\":\"205169870\"}"
37
+ yizhuan execute --ability hot_topics --platform douyin
38
+ yizhuan execute --ability hot_topics --params '{"platform":"douyin"}'
39
+ yizhuan execute --ability accounts --params '{"mode":"public_search","platform":"toutiao","keyword":"张三"}'
40
+ yizhuan execute --ability radar --params '{"mode":"following"}'
41
+ yizhuan execute --ability radar --mode timeline --timeRange 7d
42
+ yizhuan execute --ability radar --params '{"mode":"unfollow","articleId":"205169870"}'
43
+ ```
44
+
45
+ ### PowerShell 参数说明
46
+
47
+ PowerShell 对 JSON 双引号转义不友好。推荐以下写法:
48
+
49
+ ```powershell
50
+ # 最稳妥:独立参数
51
+ yizhuan execute --ability hot_topics --platform douyin
52
+
53
+ # 单引号包 JSON
54
+ yizhuan execute --ability hot_topics --params '{"platform":"douyin"}'
55
+
56
+ # key=value
57
+ yizhuan execute --ability hot_topics --params platform=douyin
58
+ ```
59
+
60
+ 请避免 bash 风格的:
61
+
62
+ ```powershell
63
+ # 在 PowerShell 中容易失败
64
+ yizhuan execute --ability hot_topics --params "{\"platform\":\"douyin\"}"
41
65
  ```
42
66
 
43
67
  ## 支持的典型能力
44
68
 
45
69
  - 作品查询:`yizhuan execute --ability works --query "AI 相关的热门作品有哪些"`
46
70
  - 作者查询:`yizhuan execute --ability accounts --query "查询张三这个作者"`
47
- - 作者全网搜索:`yizhuan execute --ability accounts --params "{\"mode\":\"public_search\",\"platform\":\"toutiao\",\"keyword\":\"张三\"}"`
48
- - 关注全网作者:`yizhuan execute --ability accounts --params "{\"mode\":\"follow_public_author\",\"platform\":\"toutiao\",\"keyword\":\"张三\",\"extAuthorId\":\"author-1\"}"`
49
- - 热点查询:`yizhuan execute --ability hot_topics --query "查询头条平台热点话题"`
50
- - 雷达查询:`yizhuan execute --ability radar --params "{\"mode\":\"following\"}"`
51
- - 竞品动态:`yizhuan execute --ability radar --params "{\"mode\":\"timeline\",\"timeRange\":\"7d\"}"`
52
- - 竞品动态筛选:`yizhuan execute --ability radar --params "{\"mode\":\"timeline\",\"timeRange\":\"7d\",\"platform\":\"baijiahao\",\"keyword\":\"AI\"}"`
53
- - 取消关注雷达作者:先查询关注列表拿到 `articleId`,再执行 `yizhuan execute --ability radar --params "{\"mode\":\"unfollow\",\"articleId\":\"205169870\"}"`
71
+ - 作者全网搜索:`yizhuan execute --ability accounts --params '{"mode":"public_search","platform":"toutiao","keyword":"张三"}'`
72
+ - 关注全网作者:`yizhuan execute --ability accounts --params '{"mode":"follow_public_author","platform":"toutiao","keyword":"张三","extAuthorId":"author-1"}'`
73
+ - 热点查询:`yizhuan execute --ability hot_topics --platform douyin`
74
+ - 雷达查询:`yizhuan execute --ability radar --params '{"mode":"following"}'`
75
+ - 竞品动态:`yizhuan execute --ability radar --mode timeline --timeRange 7d`
76
+ - 竞品动态筛选:`yizhuan execute --ability radar --params '{"mode":"timeline","timeRange":"7d","platform":"baijiahao","keyword":"AI"}'`
77
+ - 取消关注雷达作者:先查询关注列表拿到 `articleId`,再执行 `yizhuan execute --ability radar --params '{"mode":"unfollow","articleId":"205169870"}'`
54
78
  - 会员查询:`yizhuan execute --ability membership --query "我的会员什么时候到期"`
55
79
 
56
80
  如果你是通过 Codex、Cursor、Claude Code 这类 Agent 使用 CLI,也可以直接用自然语言提问,例如:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yizhuan-cli/cli",
3
- "version": "0.1.3-beta.3",
3
+ "version": "0.1.4",
4
4
  "description": "易撰命令行工具,用于通过 API Key 查询易撰真实数据。",
5
5
  "private": false,
6
6
  "type": "module",
@@ -14,7 +14,9 @@
14
14
  "scripts": {
15
15
  "start": "node src/index.js",
16
16
  "pack:dry-run": "npm pack --dry-run",
17
- "smoke": "node src/index.js --help && node src/index.js --version"
17
+ "smoke": "node src/index.js --help && node --test src/version.test.js",
18
+ "test:params": "node --test src/params.test.js",
19
+ "test:version": "node --test src/version.test.js"
18
20
  },
19
21
  "engines": {
20
22
  "node": ">=22"
package/src/index.js CHANGED
@@ -25,22 +25,47 @@ const ABILITIES = new Set([
25
25
  'favorites_query'
26
26
  ])
27
27
 
28
+ const PARAM_FLAG_KEYS = [
29
+ 'platform',
30
+ 'mode',
31
+ 'keyword',
32
+ 'page',
33
+ 'pageSize',
34
+ 'timeRange',
35
+ 'date',
36
+ 'startDate',
37
+ 'endDate',
38
+ 'articleId',
39
+ 'authorId',
40
+ 'extAuthorId',
41
+ 'collectionType',
42
+ 'metric'
43
+ ]
44
+
28
45
  function printHelp() {
29
46
  console.log(`易撰 CLI
30
47
 
31
48
  Usage:
32
49
  yizhuan --help
33
50
  yizhuan --version
34
- yizhuan execute --ability <ability> [--query <text>] [--params <json>]
51
+ yizhuan execute --ability <ability> [--query <text>] [--params <json>] [--platform <name>]
52
+ yizhuan query --json '<request JSON>' [--format json|table|markdown]
35
53
  yizhuan config path
36
54
  yizhuan config show
37
55
 
38
56
  Examples:
39
57
  yizhuan execute --ability works --query "百家号昨天新增了哪些作品"
40
- yizhuan execute --ability accounts --params "{\\"mode\\":\\"public_search\\",\\"platform\\":\\"toutiao\\",\\"keyword\\":\\"张三\\"}"
41
- yizhuan execute --ability radar --params "{\\"mode\\":\\"following\\"}"
42
- yizhuan execute --ability radar --params "{\\"mode\\":\\"timeline\\",\\"timeRange\\":\\"7d\\"}"
43
- yizhuan execute --ability radar --params "{\\"mode\\":\\"unfollow\\",\\"articleId\\":\\"205169870\\"}"
58
+ yizhuan execute --ability hot_topics --platform douyin
59
+ yizhuan execute --ability hot_topics --params '{"platform":"douyin"}'
60
+ yizhuan execute --ability accounts --params '{"mode":"public_search","platform":"toutiao","keyword":"张三"}'
61
+ yizhuan execute --ability radar --params '{"mode":"following"}'
62
+ yizhuan execute --ability radar --mode timeline --timeRange 7d
63
+ yizhuan execute --ability radar --params '{"mode":"unfollow","articleId":"205169870"}'
64
+
65
+ Notes:
66
+ - PowerShell 推荐优先使用 --platform / --mode 等独立参数,或单引号 JSON:--params '{"platform":"douyin"}'
67
+ - bash/zsh 也可用:--params '{"platform":"douyin"}' 或 --platform douyin
68
+ - 兼容 key=value:--params platform=douyin
44
69
 
45
70
  Abilities:
46
71
  works 作品查询
@@ -65,22 +90,84 @@ function readPackageVersion() {
65
90
  }
66
91
  }
67
92
 
93
+ function isBalancedJsonLike(text) {
94
+ let brace = 0
95
+ let bracket = 0
96
+ let quote = null
97
+ let escaped = false
98
+
99
+ for (const char of text) {
100
+ if (escaped) {
101
+ escaped = false
102
+ continue
103
+ }
104
+ if (char === '\\') {
105
+ escaped = true
106
+ continue
107
+ }
108
+ if (quote) {
109
+ if (char === quote) quote = null
110
+ continue
111
+ }
112
+ if (char === '"' || char === "'") {
113
+ quote = char
114
+ continue
115
+ }
116
+ if (char === '{') brace += 1
117
+ if (char === '}') brace -= 1
118
+ if (char === '[') bracket += 1
119
+ if (char === ']') bracket -= 1
120
+ if (brace < 0 || bracket < 0) return false
121
+ }
122
+
123
+ return brace === 0 && bracket === 0 && quote === null
124
+ }
125
+
126
+ function readFlagValue(argv, startIndex, key) {
127
+ let index = startIndex
128
+ let raw = argv[index]
129
+ if (raw == null) return { value: true, nextIndex: startIndex - 1 }
130
+
131
+ if (key === 'params' && /[{\[]/.test(raw)) {
132
+ let joined = raw
133
+ while (!isBalancedJsonLike(joined) && index + 1 < argv.length && !String(argv[index + 1]).startsWith('--')) {
134
+ index += 1
135
+ joined += argv[index]
136
+ }
137
+ return { value: joined, nextIndex: index }
138
+ }
139
+
140
+ return { value: raw, nextIndex: index }
141
+ }
142
+
68
143
  function parseArgs(argv) {
69
144
  const args = { _: [] }
70
145
  for (let index = 0; index < argv.length; index += 1) {
71
146
  const value = argv[index]
72
- if (value.startsWith('--')) {
73
- const key = value.slice(2)
74
- const next = argv[index + 1]
75
- if (!next || next.startsWith('--')) {
76
- args[key] = true
77
- } else {
78
- args[key] = next
79
- index += 1
80
- }
81
- } else {
147
+ if (!value.startsWith('--')) {
82
148
  args._.push(value)
149
+ continue
150
+ }
151
+
152
+ const body = value.slice(2)
153
+ const eqIndex = body.indexOf('=')
154
+ if (eqIndex > 0) {
155
+ const key = body.slice(0, eqIndex)
156
+ const raw = body.slice(eqIndex + 1)
157
+ args[key] = raw
158
+ continue
159
+ }
160
+
161
+ const key = body
162
+ const next = argv[index + 1]
163
+ if (!next || next.startsWith('--')) {
164
+ args[key] = true
165
+ continue
83
166
  }
167
+
168
+ const read = readFlagValue(argv, index + 1, key)
169
+ args[key] = read.value
170
+ index = read.nextIndex
84
171
  }
85
172
  return args
86
173
  }
@@ -101,13 +188,107 @@ function readConfig() {
101
188
  }
102
189
  }
103
190
 
104
- function parseParams(value) {
105
- if (!value) return undefined
106
- try {
107
- return JSON.parse(value)
108
- } catch {
109
- throw new Error('--params 必须是 JSON,例如:--params "{\\"page\\":1}"')
191
+ function stripWrappingQuotes(text) {
192
+ const value = String(text).trim()
193
+ if (
194
+ (value.startsWith('"') && value.endsWith('"')) ||
195
+ (value.startsWith("'") && value.endsWith("'"))
196
+ ) {
197
+ return value.slice(1, -1)
198
+ }
199
+ return value
200
+ }
201
+
202
+ function coerceScalar(value) {
203
+ const text = stripWrappingQuotes(value).trim()
204
+ if (text === 'true') return true
205
+ if (text === 'false') return false
206
+ if (text === 'null') return null
207
+ if (text !== '' && /^-?\d+(\.\d+)?$/.test(text)) return Number(text)
208
+ return text
209
+ }
210
+
211
+ function sanitizeJsonLike(text) {
212
+ return String(text)
213
+ .replace(/^\uFEFF/, '')
214
+ .replace(/[\u201C\u201D]/g, '"')
215
+ .replace(/[\u2018\u2019]/g, "'")
216
+ .replace(/\\+"/g, '"')
217
+ .replace(/\\+'/g, "'")
218
+ .replace(/\\([{}:[\]:,])/g, '$1')
219
+ .replace(/\\/g, '')
220
+ .replace(/\s+/g, ' ')
221
+ .trim()
222
+ }
223
+
224
+ function parseLooseObject(text) {
225
+ let body = String(text).trim()
226
+ if (!body) return undefined
227
+
228
+ if ((body.startsWith('{') && body.endsWith('}')) || (body.startsWith('[') && body.endsWith(']'))) {
229
+ body = body.slice(1, -1).trim()
110
230
  }
231
+ if (!body) return {}
232
+
233
+ const pairs = body
234
+ .split(',')
235
+ .map((part) => part.trim())
236
+ .filter(Boolean)
237
+ if (!pairs.length) return undefined
238
+
239
+ const result = {}
240
+ for (const pair of pairs) {
241
+ const matched = pair.match(
242
+ /^(?:["']?)([A-Za-z_][\w-]*)(?:["']?)\s*[:=]\s*(.+)$/
243
+ )
244
+ if (!matched) return undefined
245
+ result[matched[1]] = coerceScalar(matched[2])
246
+ }
247
+ return result
248
+ }
249
+
250
+ export function parseParams(value) {
251
+ if (value == null || value === true || value === false) return undefined
252
+ if (typeof value === 'object' && !Array.isArray(value)) return value
253
+
254
+ const original = String(value).trim()
255
+ if (!original) return undefined
256
+
257
+ const candidates = [original, sanitizeJsonLike(original), original.replace(/\\/g, '')]
258
+ for (const candidate of candidates) {
259
+ try {
260
+ const parsed = JSON.parse(candidate)
261
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed
262
+ } catch {
263
+ // continue
264
+ }
265
+ }
266
+
267
+ for (const candidate of candidates) {
268
+ const loose = parseLooseObject(candidate)
269
+ if (loose) return loose
270
+ }
271
+
272
+ throw new Error(
273
+ '--params 必须是 JSON 或 key=value。PowerShell 推荐:--platform douyin 或 --params \'{"platform":"douyin"}\''
274
+ )
275
+ }
276
+
277
+ function assignFlagParams(params, args) {
278
+ for (const key of PARAM_FLAG_KEYS) {
279
+ if (args[key] == null || args[key] === true) continue
280
+ if (params[key] != null) continue
281
+ params[key] = coerceScalar(args[key])
282
+ }
283
+ return params
284
+ }
285
+
286
+ export function buildExecuteParams(args) {
287
+ const params = {
288
+ ...(parseParams(args.params) || {})
289
+ }
290
+ assignFlagParams(params, args)
291
+ return Object.keys(params).length > 0 ? params : undefined
111
292
  }
112
293
 
113
294
  async function execute(args) {
@@ -127,8 +308,8 @@ async function execute(args) {
127
308
  body: JSON.stringify({
128
309
  json: {
129
310
  ability,
130
- query: args.query,
131
- params: parseParams(args.params)
311
+ query: typeof args.query === 'string' ? args.query : undefined,
312
+ params: buildExecuteParams(args)
132
313
  }
133
314
  })
134
315
  })
@@ -142,6 +323,28 @@ async function execute(args) {
142
323
  console.log(JSON.stringify(payload.result?.data?.json ?? payload, null, 2))
143
324
  }
144
325
 
326
+ function renderQuery(payload, format) {
327
+ if (format === 'json' || !format) return JSON.stringify(payload, null, 2)
328
+ if (payload.status === 'error') return `${payload.error.code}: ${payload.error.message}`
329
+ const items = payload.data?.items
330
+ if (!Array.isArray(items)) return JSON.stringify(payload, null, 2)
331
+ if (format === 'markdown') return items.map((item, index) => `| ${index + 1} | ${item.title || item.name || item.nickname || ''} |`).join('\n')
332
+ return items.map((item, index) => `${index + 1}\t${item.title || item.name || item.nickname || ''}`).join('\n')
333
+ }
334
+
335
+ async function query(args) {
336
+ const request = parseParams(args.json || args.params)
337
+ if (!request) throw new Error('query 必须提供 --json 请求对象')
338
+ const config = readConfig()
339
+ const response = await fetch(`${config.apiBaseUrl}/api/agent.query`, {
340
+ method: 'POST', headers: { authorization: config.apiKey, 'content-type': 'application/json', 'x-device-type': 'pc' },
341
+ body: JSON.stringify({ json: request })
342
+ })
343
+ const text = await response.text()
344
+ if (!response.ok) throw new Error(`请求失败 HTTP ${response.status}:${text}`)
345
+ console.log(renderQuery(JSON.parse(text).result?.data?.json ?? JSON.parse(text), args.format || 'json'))
346
+ }
347
+
145
348
  async function main() {
146
349
  const args = parseArgs(process.argv.slice(2))
147
350
  const command = args._[0]
@@ -171,11 +374,27 @@ async function main() {
171
374
  await execute(args)
172
375
  return
173
376
  }
377
+ if (command === 'query') {
378
+ await query(args)
379
+ return
380
+ }
174
381
 
175
382
  throw new Error(`未知命令:${command}`)
176
383
  }
177
384
 
178
- main().catch((error) => {
179
- console.error(error.message)
180
- process.exit(1)
181
- })
385
+ const isDirectRun = (() => {
386
+ const entry = process.argv[1]
387
+ if (!entry) return false
388
+ try {
389
+ return path.resolve(entry) === fileURLToPath(import.meta.url)
390
+ } catch {
391
+ return false
392
+ }
393
+ })()
394
+
395
+ if (isDirectRun) {
396
+ main().catch((error) => {
397
+ console.error(error.message)
398
+ process.exit(1)
399
+ })
400
+ }
@@ -0,0 +1,47 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { buildExecuteParams, parseParams } from './index.js'
4
+
5
+ test('parseParams accepts standard JSON', () => {
6
+ assert.deepEqual(parseParams('{"platform":"douyin"}'), { platform: 'douyin' })
7
+ })
8
+
9
+ test('parseParams repairs PowerShell-mangled escaped JSON fragments', () => {
10
+ assert.deepEqual(parseParams('{\\platform\\:\\douyin\\}'), { platform: 'douyin' })
11
+ assert.deepEqual(parseParams('{"platform":"douyin"}'.replaceAll('"', '\\"')), {
12
+ platform: 'douyin'
13
+ })
14
+ })
15
+
16
+ test('parseParams accepts key=value and loose objects', () => {
17
+ assert.deepEqual(parseParams('platform=douyin'), { platform: 'douyin' })
18
+ assert.deepEqual(parseParams('{platform:douyin,mode:list}'), {
19
+ platform: 'douyin',
20
+ mode: 'list'
21
+ })
22
+ })
23
+
24
+ test('buildExecuteParams merges top-level flags', () => {
25
+ assert.deepEqual(
26
+ buildExecuteParams({
27
+ params: '{"mode":"timeline"}',
28
+ platform: 'douyin',
29
+ timeRange: '7d'
30
+ }),
31
+ {
32
+ mode: 'timeline',
33
+ platform: 'douyin',
34
+ timeRange: '7d'
35
+ }
36
+ )
37
+ })
38
+
39
+ test('buildExecuteParams prefers explicit params keys over flags', () => {
40
+ assert.deepEqual(
41
+ buildExecuteParams({
42
+ params: 'platform=weibo',
43
+ platform: 'douyin'
44
+ }),
45
+ { platform: 'weibo' }
46
+ )
47
+ })
@@ -0,0 +1,22 @@
1
+ import assert from 'node:assert/strict'
2
+ import { execFileSync } from 'node:child_process'
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import test from 'node:test'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ const CLI_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
9
+ const ENTRY_PATH = path.join(CLI_DIR, 'src', 'index.js')
10
+ const PACKAGE_PATH = path.join(CLI_DIR, 'package.json')
11
+ const expectedVersion = JSON.parse(fs.readFileSync(PACKAGE_PATH, 'utf8')).version
12
+
13
+ for (const flag of ['--version', '-v']) {
14
+ test(`${flag} prints the package version`, () => {
15
+ const output = execFileSync(process.execPath, [ENTRY_PATH, flag], {
16
+ encoding: 'utf8'
17
+ }).trim()
18
+
19
+ assert.equal(output, expectedVersion)
20
+ assert.notEqual(output, '')
21
+ })
22
+ }