openyida 2026.5.12 → 2026.5.13-beta.1
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 +13 -8
- package/bin/yida.js +31 -17
- package/lib/ai/ai.js +497 -0
- package/lib/app/create-form.js +18 -7
- package/lib/auth/cdp-browser-login.js +14 -15
- package/lib/auth/login.js +33 -34
- package/lib/auth/org.js +8 -8
- package/lib/auth/qr-login.js +5 -12
- package/lib/core/command-manifest.js +7 -0
- package/lib/core/doctor.js +11 -4
- package/lib/core/env-manager.js +154 -18
- package/lib/core/locales/ar.js +2 -0
- package/lib/core/locales/de.js +2 -0
- package/lib/core/locales/en.js +14 -12
- package/lib/core/locales/es.js +2 -0
- package/lib/core/locales/fr.js +2 -0
- package/lib/core/locales/hi.js +2 -0
- package/lib/core/locales/ja.js +2 -0
- package/lib/core/locales/ko.js +2 -0
- package/lib/core/locales/pt.js +2 -0
- package/lib/core/locales/vi.js +2 -0
- package/lib/core/locales/zh-HK.js +2 -0
- package/lib/core/locales/zh.js +9 -7
- package/lib/core/query-data.js +77 -26
- package/lib/core/utils.js +23 -10
- package/lib/dingtalk/dingtalk-link.js +141 -0
- package/package.json +2 -1
- package/project/pages/src/demo-dingtalk-ai-solution-center.oyd.jsx +3776 -0
- package/project/prd/demo-dingtalk-ai-solution-center.md +425 -0
- package/scripts/solution-center-runner.js +368 -0
- package/yida-skills/SKILL.md +13 -1
- package/yida-skills/skills/yida-app/SKILL.md +4 -3
- package/yida-skills/skills/yida-create-form-page/SKILL.md +4 -1
- package/yida-skills/skills/yida-create-process/SKILL.md +3 -2
- package/yida-skills/skills/yida-custom-page/SKILL.md +4 -3
- package/yida-skills/skills/yida-data-management/SKILL.md +12 -5
- package/yida-skills/skills/yida-process-rule/SKILL.md +2 -1
- package/yida-skills/skills/yida-process-rule/references/examples.md +10 -10
- package/yida-skills/skills/yida-report/SKILL.md +2 -0
- 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
|
|
@@ -316,6 +318,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
|
|
|
316
318
|
| `openyida integration enable <appType> <formUuid> <processCode>` | Enable an automation flow |
|
|
317
319
|
| `openyida integration disable <appType> <formUuid> <processCode>` | Disable an automation flow |
|
|
318
320
|
| `openyida dws <command> [args]` | Access DingTalk CLI capabilities such as contacts, calendar, todo, and approval |
|
|
321
|
+
| `openyida dingtalk-link <url> [--target fullScreen] [--legacy-scheme] [--json]` | Generate DingTalk AppLink URLs for opening pages in DingTalk; use `--legacy-scheme` only when old `dingtalk://` links are required |
|
|
319
322
|
|
|
320
323
|
### Utilities
|
|
321
324
|
|
|
@@ -328,6 +331,8 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
|
|
|
328
331
|
| `openyida update` | Update OpenYida through npm |
|
|
329
332
|
| `openyida export-conversation [options]` | Export AI conversation history |
|
|
330
333
|
| `openyida flash-to-prd --file <path> --name "<project>"` | Convert flash notes or meeting notes into a PRD prompt |
|
|
334
|
+
| `openyida ai text --prompt "..."` | Call Yida's text generation AI API |
|
|
335
|
+
| `openyida ai image --file <image> --app-type APP_XXX` | Upload an image and call the image recognition connector |
|
|
331
336
|
| `openyida cdn-config` | Configure image upload to Aliyun OSS/CDN |
|
|
332
337
|
| `openyida cdn-upload <image-path>` | Upload an image to CDN |
|
|
333
338
|
| `openyida cdn-refresh [options]` | Refresh CDN cache |
|
package/bin/yida.js
CHANGED
|
@@ -262,6 +262,15 @@ function shouldUseAgentLogin(cliArgs) {
|
|
|
262
262
|
return isAgentConversationEnvironment();
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
function shouldUsePlaywrightFallbackInAgentLogin() {
|
|
266
|
+
const { detectActiveTool } = require('../lib/core/utils');
|
|
267
|
+
const activeTool = detectActiveTool();
|
|
268
|
+
if (activeTool && activeTool.tool === 'wukong') {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
return process.env.OPENYIDA_AGENT_PLAYWRIGHT_FALLBACK === '1';
|
|
272
|
+
}
|
|
273
|
+
|
|
265
274
|
function shouldUseCodexQrLogin(cliArgs) {
|
|
266
275
|
if (cliArgs.includes('--codex-qr') || cliArgs.includes('--agent-qr')) {return true;}
|
|
267
276
|
return false;
|
|
@@ -361,23 +370,13 @@ async function main() {
|
|
|
361
370
|
const result = checkLoginOnly({ includeSecrets: args.includes('--with-cookies') });
|
|
362
371
|
console.log(JSON.stringify(result, null, 2));
|
|
363
372
|
} else if (shouldUseCodexQrLogin(args)) {
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
} else {
|
|
368
|
-
const { startCodexQrLogin } = require('../lib/auth/qr-login');
|
|
369
|
-
const result = await startCodexQrLogin({ corpId: getArgValue(args, '--corp-id') });
|
|
370
|
-
printLoginResult(result);
|
|
371
|
-
}
|
|
373
|
+
const { startCodexQrLogin } = require('../lib/auth/qr-login');
|
|
374
|
+
const result = await startCodexQrLogin({ corpId: getArgValue(args, '--corp-id') });
|
|
375
|
+
printLoginResult(result);
|
|
372
376
|
} else if (args.includes('--browser')) {
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
} else {
|
|
377
|
-
const { interactiveLogin } = require('../lib/auth/login');
|
|
378
|
-
const result = interactiveLogin();
|
|
379
|
-
printLoginResult(result);
|
|
380
|
-
}
|
|
377
|
+
const { interactiveLogin } = require('../lib/auth/login');
|
|
378
|
+
const result = interactiveLogin({ force: true });
|
|
379
|
+
printLoginResult(result);
|
|
381
380
|
} else if (args.includes('--qoder') || args.includes('--wukong')) {
|
|
382
381
|
const { codexLogin } = require('../lib/auth/codex-login');
|
|
383
382
|
const result = await codexLogin({ tool: args.includes('--qoder') ? 'qoder' : 'wukong' });
|
|
@@ -392,7 +391,9 @@ async function main() {
|
|
|
392
391
|
printLoginResult(cachedResult);
|
|
393
392
|
} else {
|
|
394
393
|
const { interactiveLogin } = require('../lib/auth/login');
|
|
395
|
-
const browserResult = interactiveLogin({
|
|
394
|
+
const browserResult = interactiveLogin({
|
|
395
|
+
playwrightFallback: shouldUsePlaywrightFallbackInAgentLogin(),
|
|
396
|
+
});
|
|
396
397
|
if (browserResult) {
|
|
397
398
|
printLoginResult(browserResult);
|
|
398
399
|
} else {
|
|
@@ -845,6 +846,12 @@ async function main() {
|
|
|
845
846
|
break;
|
|
846
847
|
}
|
|
847
848
|
|
|
849
|
+
case 'ai': {
|
|
850
|
+
const { run: runAI } = require('../lib/ai/ai');
|
|
851
|
+
await runAI(args);
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
|
|
848
855
|
case 'integration': {
|
|
849
856
|
const subCommand = args[0];
|
|
850
857
|
const subArgs = args.slice(1); // 路由层消费 subCommand,传递剩余参数
|
|
@@ -879,6 +886,13 @@ async function main() {
|
|
|
879
886
|
await runDws(args);
|
|
880
887
|
break;
|
|
881
888
|
}
|
|
889
|
+
|
|
890
|
+
case 'dingtalk-link': {
|
|
891
|
+
const { run: runDingTalkLink } = require('../lib/dingtalk/dingtalk-link');
|
|
892
|
+
await runDingTalkLink(args);
|
|
893
|
+
break;
|
|
894
|
+
}
|
|
895
|
+
|
|
882
896
|
case 'export-conversation': {
|
|
883
897
|
const { exportConversation } = require('../lib/conversation/export-conversation');
|
|
884
898
|
// 解析选项
|
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
|
+
};
|
package/lib/app/create-form.js
CHANGED
|
@@ -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
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
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;
|