openyida 2026.5.13-beta.0 → 2026.5.13

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.
Files changed (37) hide show
  1. package/README.md +13 -8
  2. package/bin/yida.js +19 -17
  3. package/lib/ai/ai.js +497 -0
  4. package/lib/app/create-form.js +18 -7
  5. package/lib/auth/login.js +25 -20
  6. package/lib/basic-info/basic-info.js +407 -0
  7. package/lib/core/command-manifest.js +6 -0
  8. package/lib/core/locales/ar.js +3 -1
  9. package/lib/core/locales/de.js +3 -1
  10. package/lib/core/locales/en.js +14 -12
  11. package/lib/core/locales/es.js +3 -1
  12. package/lib/core/locales/fr.js +3 -1
  13. package/lib/core/locales/hi.js +3 -1
  14. package/lib/core/locales/ja.js +9 -7
  15. package/lib/core/locales/ko.js +3 -1
  16. package/lib/core/locales/pt.js +3 -1
  17. package/lib/core/locales/vi.js +3 -1
  18. package/lib/core/locales/zh-HK.js +11 -9
  19. package/lib/core/locales/zh.js +14 -12
  20. package/lib/core/query-data.js +77 -26
  21. package/lib/core/utils.js +23 -10
  22. package/package.json +2 -1
  23. package/project/pages/src/demo-dingtalk-ai-solution-center.oyd.jsx +3960 -0
  24. package/project/prd/demo-dingtalk-ai-solution-center.md +425 -0
  25. package/scripts/e2e-real/skill-coverage.js +1 -0
  26. package/scripts/solution-center-runner.js +368 -0
  27. package/yida-skills/SKILL.md +14 -1
  28. package/yida-skills/skills/yida-app/SKILL.md +4 -3
  29. package/yida-skills/skills/yida-basic-info/SKILL.md +137 -0
  30. package/yida-skills/skills/yida-create-form-page/SKILL.md +4 -1
  31. package/yida-skills/skills/yida-create-process/SKILL.md +3 -2
  32. package/yida-skills/skills/yida-custom-page/SKILL.md +4 -3
  33. package/yida-skills/skills/yida-data-management/SKILL.md +12 -5
  34. package/yida-skills/skills/yida-process-rule/SKILL.md +2 -1
  35. package/yida-skills/skills/yida-process-rule/references/examples.md +10 -10
  36. package/yida-skills/skills/yida-report/SKILL.md +2 -0
  37. package/yida-skills/skills/yida-report/references/examples.md +2 -2
package/README.md CHANGED
@@ -167,8 +167,8 @@ openyida/
167
167
  openyida create-app "CRM"
168
168
  openyida create-app --name "CRM" --desc "Customer management" --theme deepBlue
169
169
  openyida app-list --size 20
170
- openyida create-form create APP_XXX "Customer" fields.json
171
- openyida create-form update APP_XXX FORM_XXX changes.json
170
+ openyida create-form create APP_XXX "Customer" .cache/openyida/forms/customer-fields.json
171
+ openyida create-form update APP_XXX FORM_XXX .cache/openyida/forms/customer-changes.json
172
172
  openyida get-schema APP_XXX FORM_XXX
173
173
  openyida get-schema APP_XXX --all --output-dir .cache/schemas
174
174
  ```
@@ -177,7 +177,7 @@ openyida get-schema APP_XXX --all --output-dir .cache/schemas
177
177
 
178
178
  ```bash
179
179
  openyida create-page APP_XXX "Dashboard" --mode dashboard
180
- openyida generate-page product-homepage --spec page.json --output pages/src/home.oyd.jsx --compile
180
+ openyida generate-page product-homepage --spec .cache/openyida/page-specs/home.json --output pages/src/home.oyd.jsx --compile
181
181
  openyida generate-page todo-mvc --output pages/src/todo-mvc.oyd.jsx --compile
182
182
  openyida check-page pages/src/home.oyd.jsx
183
183
  openyida compile pages/src/home.oyd.jsx
@@ -190,14 +190,16 @@ Built-in templates currently include `product-homepage` for product/portal pages
190
190
  ### Workflow, Data, and Permissions
191
191
 
192
192
  ```bash
193
- openyida create-process APP_XXX "Purchase Request" fields.json process.json
194
- openyida configure-process APP_XXX FORM_XXX process.json
195
- openyida process preview APP_XXX PROC_INST_XXX --output process.html
193
+ openyida create-process APP_XXX "Purchase Request" .cache/openyida/process/fields.json .cache/openyida/process/process.json
194
+ openyida configure-process APP_XXX FORM_XXX .cache/openyida/process/process.json
195
+ openyida process preview APP_XXX PROC_INST_XXX --output .cache/openyida/process/process.html
196
196
  openyida data query form APP_XXX FORM_XXX --page 1 --size 20
197
+ openyida data create form APP_XXX FORM_XXX --data-file .cache/openyida/data-import/record.json
197
198
  openyida get-permission APP_XXX FORM_XXX
198
199
  ```
199
200
 
200
201
  When creating or updating test data with `openyida data`, Yida date fields must use 13-digit millisecond timestamps, for example `"dateField_xxx": 1719705600000`. Do not submit `YYYY-MM-DD` strings for `DateField` or `CascadeDateField` values.
202
+ Temporary JSON, CSV, and one-off import scripts should live under `.cache/openyida/` so generated run artifacts do not clutter the repository root.
201
203
 
202
204
  ### Real Environment E2E
203
205
 
@@ -239,8 +241,8 @@ Use `npm run test:e2e:real:cleanup` to list recorded disposable resources. OpenY
239
241
  openyida connector smart-create --curl "curl https://api.example.com/users"
240
242
  openyida connector list
241
243
  openyida integration create APP_XXX FORM_XXX "Sync customer data"
242
- openyida create-report APP_XXX "Sales Dashboard" charts.json
243
- openyida append-chart APP_XXX REPORT_XXX chart.json
244
+ openyida create-report APP_XXX "Sales Dashboard" .cache/openyida/reports/charts.json
245
+ openyida append-chart APP_XXX REPORT_XXX .cache/openyida/reports/chart.json
244
246
  ```
245
247
 
246
248
  ## CLI Reference
@@ -294,6 +296,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
294
296
  | `openyida data <action> <resource> [args]` | Unified data management for forms, processes, tasks, and subforms |
295
297
  | `openyida data check <appType> <formUuid> <rules.json>` | Detect anomalous process-form records |
296
298
  | `openyida task-center <type> [options]` | Query todo, created, processed, CC, or proxy-submitted tasks |
299
+ | `openyida basic-info <overview\|commodity\|grant\|capacity\|quota\|abs-path\|dataflow\|i18n\|domain>` | Query organization basic info, capacity, quotas, fixed-domain records, and domain settings |
297
300
  | `openyida get-permission <appType> <formUuid>` | Query form permission configuration |
298
301
  | `openyida save-permission <appType> <formUuid> [options]` | Save form permission configuration |
299
302
  | `openyida corp-manager <sub-command>` | Manage platform admins, sub-admins, app admins, and address book visibility |
@@ -329,6 +332,8 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
329
332
  | `openyida update` | Update OpenYida through npm |
330
333
  | `openyida export-conversation [options]` | Export AI conversation history |
331
334
  | `openyida flash-to-prd --file <path> --name "<project>"` | Convert flash notes or meeting notes into a PRD prompt |
335
+ | `openyida ai text --prompt "..."` | Call Yida's text generation AI API |
336
+ | `openyida ai image --file <image> --app-type APP_XXX` | Upload an image and call the image recognition connector |
332
337
  | `openyida cdn-config` | Configure image upload to Aliyun OSS/CDN |
333
338
  | `openyida cdn-upload <image-path>` | Upload an image to CDN |
334
339
  | `openyida cdn-refresh [options]` | Refresh CDN cache |
package/bin/yida.js CHANGED
@@ -123,7 +123,7 @@ function printHelp() {
123
123
  console.log(`\n ${BOLD}${CYAN}${t('help.quickstart_title')}${RESET}`);
124
124
  console.log(` ${DIM}${RESET} openyida login`);
125
125
  console.log(` ${DIM}${RESET} openyida create-app "${t('help.quickstart_app_name')}"`);
126
- console.log(` ${DIM}${RESET} openyida create-form create APP_XXX "${t('help.quickstart_form_name')}" fields.json`);
126
+ console.log(` ${DIM}${RESET} openyida create-form create APP_XXX "${t('help.quickstart_form_name')}" .cache/openyida/forms/fields.json`);
127
127
  console.log(` ${DIM}${RESET} openyida dws contact user search --keyword "张三"`);
128
128
  console.log('');
129
129
  console.log(` ${DIM}${t('help.docs')} https://openyida.ai · https://github.com/openyida/openyida${RESET}`);
@@ -370,23 +370,13 @@ async function main() {
370
370
  const result = checkLoginOnly({ includeSecrets: args.includes('--with-cookies') });
371
371
  console.log(JSON.stringify(result, null, 2));
372
372
  } else if (shouldUseCodexQrLogin(args)) {
373
- const cachedResult = checkLoginOnly({ includeSecrets: true });
374
- if (cachedResult.status === 'ok') {
375
- printLoginResult(cachedResult);
376
- } else {
377
- const { startCodexQrLogin } = require('../lib/auth/qr-login');
378
- const result = await startCodexQrLogin({ corpId: getArgValue(args, '--corp-id') });
379
- printLoginResult(result);
380
- }
373
+ const { startCodexQrLogin } = require('../lib/auth/qr-login');
374
+ const result = await startCodexQrLogin({ corpId: getArgValue(args, '--corp-id') });
375
+ printLoginResult(result);
381
376
  } else if (args.includes('--browser')) {
382
- const cachedResult = checkLoginOnly({ includeSecrets: true });
383
- if (cachedResult.status === 'ok') {
384
- printLoginResult(cachedResult);
385
- } else {
386
- const { interactiveLogin } = require('../lib/auth/login');
387
- const result = interactiveLogin();
388
- printLoginResult(result);
389
- }
377
+ const { interactiveLogin } = require('../lib/auth/login');
378
+ const result = interactiveLogin({ force: true });
379
+ printLoginResult(result);
390
380
  } else if (args.includes('--qoder') || args.includes('--wukong')) {
391
381
  const { codexLogin } = require('../lib/auth/codex-login');
392
382
  const result = await codexLogin({ tool: args.includes('--qoder') ? 'qoder' : 'wukong' });
@@ -665,6 +655,12 @@ async function main() {
665
655
  break;
666
656
  }
667
657
 
658
+ case 'basic-info': {
659
+ const { run: runBasicInfo } = require('../lib/basic-info/basic-info');
660
+ await runBasicInfo(args);
661
+ break;
662
+ }
663
+
668
664
  case 'doctor': {
669
665
  const { run } = require('../lib/core/doctor');
670
666
  await run(args);
@@ -856,6 +852,12 @@ async function main() {
856
852
  break;
857
853
  }
858
854
 
855
+ case 'ai': {
856
+ const { run: runAI } = require('../lib/ai/ai');
857
+ await runAI(args);
858
+ break;
859
+ }
860
+
859
861
  case 'integration': {
860
862
  const subCommand = args[0];
861
863
  const subArgs = args.slice(1); // 路由层消费 subCommand,传递剩余参数
package/lib/ai/ai.js ADDED
@@ -0,0 +1,497 @@
1
+ /**
2
+ * ai.js - 宜搭 AI 能力命令
3
+ *
4
+ * 支持:
5
+ * openyida ai text --prompt "..." 调用 txtFromAI 文生文
6
+ * openyida ai image --file ./image.png 上传图片并调用识图连接器
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const crypto = require('crypto');
14
+ const querystring = require('querystring');
15
+ const {
16
+ loadCookieData,
17
+ triggerLogin,
18
+ resolveBaseUrl,
19
+ findProjectRoot,
20
+ httpGet,
21
+ httpPost,
22
+ requestWithAutoLogin,
23
+ } = require('../core/utils');
24
+ const { warn } = require('../core/chalk');
25
+
26
+ const DEFAULT_MAX_TOKENS = 3000;
27
+ const DEFAULT_IMAGE_CONNECTOR = {
28
+ connectorId: 'Http_2aa221179eef4c128de666c5b9c8df1b',
29
+ actionId: 'flowerrecognize',
30
+ connection: 2391,
31
+ };
32
+
33
+ const IMAGE_MIME_TYPES = {
34
+ '.png': 'image/png',
35
+ '.jpg': 'image/jpeg',
36
+ '.jpeg': 'image/jpeg',
37
+ '.gif': 'image/gif',
38
+ '.webp': 'image/webp',
39
+ '.bmp': 'image/bmp',
40
+ };
41
+
42
+ function printHelp() {
43
+ console.log(`
44
+ 用法:
45
+ openyida ai text --prompt "提示词" [--max-tokens 3000] [--json]
46
+ openyida ai text --file prompt.txt [--json]
47
+ openyida ai image --file ./image.png --app-type APP_XXX [--json]
48
+ openyida ai image --image-url https://... [--json]
49
+
50
+ 子命令:
51
+ text 调用宜搭 /query/intelligent/txtFromAI.json
52
+ image 上传本地图片并调用识图连接器;也可直接传入 --image-url
53
+
54
+ image 选项:
55
+ --app-type <APP_XXX> 上传图片使用的宜搭应用 ID;未传时尝试读取当前 project/config.json 或 Cookie
56
+ --form-uuid <FORM_XXX> 可选,上传回调关联的表单 ID
57
+ --connector-id <id> 识图 HTTP 连接器 ID,默认使用 HAR 中的植物识别连接器
58
+ --action-id <id> 识图动作 ID,默认 flowerrecognize
59
+ --connection <id> 识图连接账号 ID,默认 2391
60
+ --baike / --no-baike 是否返回百科信息,默认 --baike
61
+ --base-url <url> 覆盖宜搭域名,例如 https://demo.aliwork.com
62
+ `);
63
+ }
64
+
65
+ function parseArgs(args) {
66
+ const parsed = {
67
+ subCommand: args[0],
68
+ prompt: '',
69
+ file: '',
70
+ imageUrl: '',
71
+ maxTokens: DEFAULT_MAX_TOKENS,
72
+ json: false,
73
+ appType: '',
74
+ formUuid: '',
75
+ connectorId: DEFAULT_IMAGE_CONNECTOR.connectorId,
76
+ actionId: DEFAULT_IMAGE_CONNECTOR.actionId,
77
+ connection: DEFAULT_IMAGE_CONNECTOR.connection,
78
+ baike: true,
79
+ baseUrl: '',
80
+ help: false,
81
+ };
82
+
83
+ for (let i = 1; i < args.length; i++) {
84
+ const arg = args[i];
85
+ if (arg === '--help' || arg === '-h') {
86
+ parsed.help = true;
87
+ } else if ((arg === '--prompt' || arg === '-p') && args[i + 1]) {
88
+ parsed.prompt = args[++i];
89
+ } else if ((arg === '--file' || arg === '-f') && args[i + 1]) {
90
+ parsed.file = args[++i];
91
+ } else if (arg === '--image-url' && args[i + 1]) {
92
+ parsed.imageUrl = args[++i];
93
+ } else if (arg === '--max-tokens' && args[i + 1]) {
94
+ parsed.maxTokens = parseInt(args[++i], 10) || DEFAULT_MAX_TOKENS;
95
+ } else if (arg === '--json') {
96
+ parsed.json = true;
97
+ } else if (arg === '--app-type' && args[i + 1]) {
98
+ parsed.appType = args[++i];
99
+ } else if (arg === '--form-uuid' && args[i + 1]) {
100
+ parsed.formUuid = args[++i];
101
+ } else if (arg === '--connector-id' && args[i + 1]) {
102
+ parsed.connectorId = args[++i];
103
+ } else if (arg === '--action-id' && args[i + 1]) {
104
+ parsed.actionId = args[++i];
105
+ } else if (arg === '--connection' && args[i + 1]) {
106
+ parsed.connection = parseInt(args[++i], 10) || args[i];
107
+ } else if (arg === '--baike') {
108
+ parsed.baike = true;
109
+ } else if (arg === '--no-baike') {
110
+ parsed.baike = false;
111
+ } else if (arg === '--base-url' && args[i + 1]) {
112
+ parsed.baseUrl = args[++i].replace(/\/+$/, '');
113
+ }
114
+ }
115
+
116
+ return parsed;
117
+ }
118
+
119
+ function readStdin() {
120
+ return new Promise((resolve, reject) => {
121
+ if (process.stdin.isTTY) {
122
+ resolve('');
123
+ return;
124
+ }
125
+ let data = '';
126
+ process.stdin.setEncoding('utf-8');
127
+ process.stdin.on('data', (chunk) => { data += chunk; });
128
+ process.stdin.on('end', () => resolve(data));
129
+ process.stdin.on('error', reject);
130
+ });
131
+ }
132
+
133
+ async function readPrompt(options) {
134
+ if (options.prompt) {
135
+ return options.prompt;
136
+ }
137
+ if (options.file) {
138
+ const absolutePath = path.resolve(options.file);
139
+ if (!fs.existsSync(absolutePath)) {
140
+ throw new Error(`文件不存在: ${absolutePath}`);
141
+ }
142
+ return fs.readFileSync(absolutePath, 'utf-8');
143
+ }
144
+ return readStdin();
145
+ }
146
+
147
+ function getAuthRef(options) {
148
+ let cookieData = loadCookieData();
149
+ if (!cookieData || !cookieData.cookies || !cookieData.csrf_token) {
150
+ cookieData = triggerLogin();
151
+ }
152
+ if (!cookieData || !cookieData.cookies || !cookieData.csrf_token) {
153
+ throw new Error('未获取到有效宜搭登录态,请先执行 openyida login');
154
+ }
155
+ return {
156
+ csrfToken: cookieData.csrf_token || '',
157
+ cookies: cookieData.cookies || [],
158
+ baseUrl: options.baseUrl || resolveBaseUrl(cookieData),
159
+ cookieData,
160
+ };
161
+ }
162
+
163
+ function getCookieValue(authRef, name) {
164
+ const matched = (authRef.cookies || []).filter(cookie => cookie.name === name);
165
+ return matched.length ? matched[0].value : '';
166
+ }
167
+
168
+ function inferAppType(options, authRef) {
169
+ if (options.appType) {
170
+ return options.appType;
171
+ }
172
+
173
+ const projectRoot = findProjectRoot();
174
+ const configPath = path.join(projectRoot, 'config.json');
175
+ if (fs.existsSync(configPath)) {
176
+ try {
177
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
178
+ if (config.appType) {
179
+ return config.appType;
180
+ }
181
+ } catch {
182
+ // ignore invalid local config and continue with cookie fallback
183
+ }
184
+ }
185
+
186
+ return getCookieValue(authRef, 'tianshu_app_type');
187
+ }
188
+
189
+ function getSuccessContent(response, fallbackMessage) {
190
+ if (!response || !response.success) {
191
+ throw new Error(response && response.errorMsg ? response.errorMsg : fallbackMessage);
192
+ }
193
+ return response.content;
194
+ }
195
+
196
+ async function callTextFromAI(prompt, options, authRef) {
197
+ const response = await requestWithAutoLogin((auth) => {
198
+ const postData = querystring.stringify({
199
+ _csrf_token: auth.csrfToken,
200
+ prompt,
201
+ maxTokens: String(options.maxTokens || DEFAULT_MAX_TOKENS),
202
+ skill: 'ToText',
203
+ });
204
+ return httpPost(auth.baseUrl, '/query/intelligent/txtFromAI.json', postData, auth.cookies);
205
+ }, authRef);
206
+
207
+ const content = getSuccessContent(response, 'AI 接口调用失败');
208
+
209
+ return {
210
+ success: true,
211
+ content: content && content.content ? content.content : '',
212
+ raw: response,
213
+ };
214
+ }
215
+
216
+ function getMimeType(filePath) {
217
+ const ext = path.extname(filePath).toLowerCase();
218
+ return IMAGE_MIME_TYPES[ext] || 'application/octet-stream';
219
+ }
220
+
221
+ function createObjectName(appType, filePath) {
222
+ const ext = path.extname(filePath) || '.png';
223
+ const now = new Date();
224
+ const monthDay = (now.getMonth() + 1) + '-' + now.getDate();
225
+ const id = crypto.randomUUID ? crypto.randomUUID().toUpperCase() : crypto.randomBytes(16).toString('hex').toUpperCase();
226
+ return appType + '/' + now.getFullYear() + '/' + monthDay + '/' + id + ext;
227
+ }
228
+
229
+ async function getOssSign(filePath, appType, authRef) {
230
+ const stat = fs.statSync(filePath);
231
+ const fileName = path.basename(filePath);
232
+ const contentType = getMimeType(filePath);
233
+ const objectName = createObjectName(appType, filePath);
234
+
235
+ return requestWithAutoLogin((auth) => {
236
+ return httpGet(auth.baseUrl, '/ossSign', {
237
+ scene: 'ImageField',
238
+ _api: 'nattyFetch',
239
+ _mock: 'false',
240
+ _csrf_token: auth.csrfToken,
241
+ appType,
242
+ fileName,
243
+ fileSize: String(stat.size),
244
+ contentType,
245
+ isOpen: 'n',
246
+ newContext: 'y',
247
+ objectName,
248
+ procInstId: '',
249
+ businessType: '',
250
+ accelerate: 'y',
251
+ _stamp: String(Date.now()),
252
+ }, auth.cookies);
253
+ }, authRef);
254
+ }
255
+
256
+ async function postToOss(filePath, signContent) {
257
+ if (typeof fetch !== 'function' || typeof FormData !== 'function' || typeof Blob !== 'function') {
258
+ throw new Error('当前 Node.js 环境缺少 fetch/FormData,请使用 Node.js 18+');
259
+ }
260
+
261
+ const fileName = path.basename(filePath);
262
+ const fileBuffer = fs.readFileSync(filePath);
263
+ const form = new FormData();
264
+ form.append('accessid', signContent.accessid);
265
+ form.append('key', signContent.objectName);
266
+ form.append('policy', signContent.policy);
267
+ form.append('OSSAccessKeyId', signContent.accessid);
268
+ form.append('signature', signContent.signature);
269
+ form.append('expire', signContent.expire);
270
+ form.append('appType', signContent.appType);
271
+ form.append('Content-Disposition', 'attachment; filename=' + fileName);
272
+ form.append('file', new Blob([fileBuffer], { type: getMimeType(filePath) }), fileName);
273
+
274
+ const response = await fetch(signContent.host, {
275
+ method: 'POST',
276
+ body: form,
277
+ });
278
+
279
+ if (!response.ok && response.status !== 204) {
280
+ const body = await response.text();
281
+ throw new Error('OSS 上传失败: HTTP ' + response.status + ' ' + body.slice(0, 160));
282
+ }
283
+
284
+ return {
285
+ status: response.status,
286
+ requestId: response.headers.get('x-oss-request-id') || '',
287
+ };
288
+ }
289
+
290
+ async function convertToPublicImageUrl(downloadUrl, authRef) {
291
+ const response = await requestWithAutoLogin((auth) => {
292
+ return httpGet(auth.baseUrl, '/aliyun/sdk/upload2Oss.json', {
293
+ imageUrl: downloadUrl,
294
+ _csrf_token: auth.csrfToken,
295
+ }, auth.cookies);
296
+ }, authRef);
297
+
298
+ if (!response || !response.success) {
299
+ throw new Error(response && response.errorMsg ? response.errorMsg : '图片 URL 转换失败');
300
+ }
301
+ return response.content;
302
+ }
303
+
304
+ async function uploadCallback(filePath, appType, formUuid, signContent, ossResult, authRef) {
305
+ if (!formUuid) {
306
+ return { skipped: true };
307
+ }
308
+
309
+ const stat = fs.statSync(filePath);
310
+ return requestWithAutoLogin((auth) => {
311
+ const postData = querystring.stringify({
312
+ _csrf_token: auth.csrfToken,
313
+ appType,
314
+ fileName: path.basename(filePath),
315
+ fileSize: String(stat.size),
316
+ objectName: signContent.objectName,
317
+ formUuid,
318
+ procInstId: '',
319
+ ossRequestId: ossResult.requestId || '',
320
+ businessType: 'inst',
321
+ });
322
+ return httpPost(auth.baseUrl, '/query/attach/uploadCallBack.json', postData, auth.cookies);
323
+ }, authRef);
324
+ }
325
+
326
+ async function uploadImageForAI(filePath, options, authRef) {
327
+ const absolutePath = path.resolve(filePath);
328
+ if (!fs.existsSync(absolutePath)) {
329
+ throw new Error(`图片不存在: ${absolutePath}`);
330
+ }
331
+
332
+ const appType = inferAppType(options, authRef);
333
+ if (!appType) {
334
+ throw new Error('上传图片需要 appType,请传入 --app-type APP_XXX');
335
+ }
336
+
337
+ const signResponse = await getOssSign(absolutePath, appType, authRef);
338
+ const signContent = getSuccessContent(signResponse, '获取 OSS 上传签名失败');
339
+ if (!signContent) {
340
+ throw new Error('获取 OSS 上传签名失败');
341
+ }
342
+
343
+ const ossResult = await postToOss(absolutePath, signContent);
344
+ const publicImageUrl = await convertToPublicImageUrl(signContent.downloadUrl, authRef);
345
+ const callbackResult = await uploadCallback(absolutePath, appType, options.formUuid, signContent, ossResult, authRef);
346
+
347
+ return {
348
+ appType,
349
+ fileName: path.basename(absolutePath),
350
+ fileSize: fs.statSync(absolutePath).size,
351
+ contentType: getMimeType(absolutePath),
352
+ objectName: signContent.objectName,
353
+ downloadUrl: signContent.downloadUrl,
354
+ previewUrl: signContent.previewUrl,
355
+ imageUrl: publicImageUrl,
356
+ ossRequestId: ossResult.requestId,
357
+ callback: callbackResult && callbackResult.skipped ? 'skipped' : 'ok',
358
+ };
359
+ }
360
+
361
+ async function invokeImageRecognition(imageUrl, options, authRef) {
362
+ const inputs = {
363
+ path: {},
364
+ query: {
365
+ PageIndex: '1',
366
+ PageSize: '50',
367
+ KeyWord: '',
368
+ },
369
+ header: {},
370
+ body: {
371
+ image: imageUrl,
372
+ baike: options.baike ? '1' : '0',
373
+ },
374
+ };
375
+ const serviceInfo = {
376
+ connectorInfo: {
377
+ connectorId: options.connectorId,
378
+ actionId: options.actionId,
379
+ type: 'httpConnector',
380
+ connection: options.connection,
381
+ },
382
+ };
383
+
384
+ const response = await requestWithAutoLogin((auth) => {
385
+ const postData = querystring.stringify({
386
+ inputs: JSON.stringify(inputs),
387
+ serviceInfo: JSON.stringify(serviceInfo),
388
+ _csrf_token: auth.csrfToken,
389
+ });
390
+ return httpPost(auth.baseUrl, '/query/publicService/invokeService.json', postData, auth.cookies);
391
+ }, authRef);
392
+
393
+ if (!response || !response.success) {
394
+ throw new Error(response && response.errorMsg ? response.errorMsg : '识图服务调用失败');
395
+ }
396
+
397
+ return response.content && response.content.serviceReturnValue
398
+ ? response.content.serviceReturnValue
399
+ : response.content;
400
+ }
401
+
402
+ function normalizeImageResult(serviceReturnValue) {
403
+ const list = serviceReturnValue && Array.isArray(serviceReturnValue.result)
404
+ ? serviceReturnValue.result
405
+ : [];
406
+ return list.map(item => ({
407
+ name: item.name || '',
408
+ score: typeof item.score === 'number' ? item.score : Number(item.score) || 0,
409
+ confidence: Math.round((typeof item.score === 'number' ? item.score : Number(item.score) || 0) * 10000) / 100,
410
+ baikeInfo: item.baike_info || item.baikeInfo || {},
411
+ raw: item,
412
+ }));
413
+ }
414
+
415
+ function printImageSummary(result) {
416
+ console.log('图片 URL: ' + result.imageUrl);
417
+ if (!result.recognition.length) {
418
+ console.log('识别结果: 无结果');
419
+ return;
420
+ }
421
+ console.log('识别结果:');
422
+ result.recognition.forEach((item, index) => {
423
+ console.log(` ${index + 1}. ${item.name || '未知'} ${item.confidence}%`);
424
+ });
425
+ }
426
+
427
+ async function runText(options, authRef) {
428
+ const prompt = await readPrompt(options);
429
+ if (!prompt.trim()) {
430
+ throw new Error('请通过 --prompt、--file 或 stdin 提供提示词');
431
+ }
432
+ const output = await callTextFromAI(prompt, options, authRef);
433
+ if (options.json) {
434
+ console.log(JSON.stringify(output, null, 2));
435
+ } else {
436
+ console.log(output.content);
437
+ }
438
+ }
439
+
440
+ async function runImage(options, authRef) {
441
+ let upload = null;
442
+ let imageUrl = options.imageUrl;
443
+
444
+ if (!imageUrl) {
445
+ if (!options.file) {
446
+ throw new Error('请通过 --file 上传图片,或通过 --image-url 传入图片 URL');
447
+ }
448
+ upload = await uploadImageForAI(options.file, options, authRef);
449
+ imageUrl = upload.imageUrl;
450
+ }
451
+
452
+ const serviceReturnValue = await invokeImageRecognition(imageUrl, options, authRef);
453
+ const result = {
454
+ success: true,
455
+ imageUrl,
456
+ upload,
457
+ recognition: normalizeImageResult(serviceReturnValue),
458
+ raw: serviceReturnValue,
459
+ };
460
+
461
+ if (options.json) {
462
+ console.log(JSON.stringify(result, null, 2));
463
+ } else {
464
+ printImageSummary(result);
465
+ }
466
+ }
467
+
468
+ async function run(args) {
469
+ const options = parseArgs(args);
470
+ if (!options.subCommand || options.help || options.subCommand === '--help' || options.subCommand === '-h') {
471
+ printHelp();
472
+ return;
473
+ }
474
+
475
+ const authRef = getAuthRef(options);
476
+ if (options.subCommand === 'text' || options.subCommand === 'txt') {
477
+ await runText(options, authRef);
478
+ return;
479
+ }
480
+ if (options.subCommand === 'image' || options.subCommand === 'vision' || options.subCommand === 'image-recognize') {
481
+ await runImage(options, authRef);
482
+ return;
483
+ }
484
+
485
+ warn(`未知的 ai 子命令: ${options.subCommand}`);
486
+ printHelp();
487
+ process.exit(1);
488
+ }
489
+
490
+ module.exports = {
491
+ run,
492
+ callTextFromAI,
493
+ uploadImageForAI,
494
+ invokeImageRecognition,
495
+ normalizeImageResult,
496
+ parseArgs,
497
+ };
@@ -2187,16 +2187,27 @@ async function requestWithAutoLogin(requestFn, authRef) {
2187
2187
  // 307:csrf_token 过期,刷新后重试
2188
2188
  if (result && result.__csrfExpired) {
2189
2189
  const refreshedData = refreshCsrfToken();
2190
- authRef.cookieData = refreshedData;
2191
- authRef.csrfToken = refreshedData.csrf_token;
2192
- authRef.cookies = refreshedData.cookies;
2193
- authRef.baseUrl = resolveBaseUrl(refreshedData);
2194
- info(t('common.csrf_refreshed'));
2195
- result = await requestFn(authRef);
2190
+ if (refreshedData && refreshedData.cookies && refreshedData.csrf_token) {
2191
+ authRef.cookieData = refreshedData;
2192
+ authRef.csrfToken = refreshedData.csrf_token;
2193
+ authRef.cookies = refreshedData.cookies;
2194
+ authRef.baseUrl = resolveBaseUrl(refreshedData);
2195
+ info(t('common.csrf_refreshed'));
2196
+ result = await requestFn(authRef);
2197
+ } else {
2198
+ result = { __needLogin: true };
2199
+ }
2196
2200
  }
2197
2201
  // 302/301:登录态失效,重新登录后重试
2198
2202
  if (result && result.__needLogin) {
2199
- const newCookieData = triggerLogin();
2203
+ const newCookieData = triggerLogin({ force: true });
2204
+ if (!newCookieData || !newCookieData.cookies || !newCookieData.csrf_token) {
2205
+ return {
2206
+ success: false,
2207
+ __needLogin: true,
2208
+ errorMsg: t('common.login_expired', 'openyida login --qr / openyida login --browser'),
2209
+ };
2210
+ }
2200
2211
  authRef.cookieData = newCookieData;
2201
2212
  authRef.csrfToken = newCookieData.csrf_token;
2202
2213
  authRef.cookies = newCookieData.cookies;