openyida 0.1.2 → 1.0.0-beta.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.
Files changed (61) hide show
  1. package/README.md +68 -38
  2. package/bin/yida.js +164 -761
  3. package/lib/babel-transform/index.js +244 -0
  4. package/lib/babel-transform/jsx-utils.js +89 -0
  5. package/lib/check-update.js +72 -0
  6. package/lib/copy.js +258 -0
  7. package/lib/create-app.js +174 -0
  8. package/lib/create-form.js +2244 -0
  9. package/lib/create-page.js +89 -0
  10. package/lib/env.js +164 -0
  11. package/lib/get-page-config.js +102 -0
  12. package/lib/get-schema.js +76 -0
  13. package/lib/login.js +323 -0
  14. package/lib/publish.js +610 -0
  15. package/lib/save-share-config.js +268 -0
  16. package/lib/update-form-config.js +237 -0
  17. package/lib/utils.js +443 -0
  18. package/lib/verify-short-url.js +279 -0
  19. package/package.json +20 -7
  20. package/project/.cache/demo-schema.json +2353 -0
  21. package/project/pages/src/demo-birthday-game.js +833 -0
  22. package/project/pages/src/demo-future-vision-2026.js +1102 -0
  23. package/project/pages/src/demo-salary-calculator.js +904 -0
  24. package/project/prd/demo-birthday-game.md +39 -0
  25. package/project/prd/demo-future-vision-2026.md +78 -0
  26. package/project/prd/demo-salary-calculator.md +101 -0
  27. package/scripts/postinstall.js +114 -0
  28. package/yida-skills/SKILL.md +273 -0
  29. package/yida-skills/reference/association-form-field.md +469 -0
  30. package/yida-skills/reference/employee-field.md +17 -0
  31. package/yida-skills/reference/model-api.md +73 -0
  32. package/yida-skills/reference/serial-number-field.md +132 -0
  33. package/yida-skills/reference/yida-api.md +1208 -0
  34. package/yida-skills/skills/yida-app/SKILL.md +394 -0
  35. package/yida-skills/skills/yida-create-app/SKILL.md +158 -0
  36. package/yida-skills/skills/yida-create-form-page/SKILL.md +598 -0
  37. package/yida-skills/skills/yida-create-page/SKILL.md +103 -0
  38. package/yida-skills/skills/yida-custom-page/SKILL.md +533 -0
  39. package/yida-skills/skills/yida-get-schema/SKILL.md +90 -0
  40. package/yida-skills/skills/yida-login/SKILL.md +200 -0
  41. package/yida-skills/skills/yida-logout/SKILL.md +58 -0
  42. package/yida-skills/skills/yida-page-config/SKILL.md +261 -0
  43. package/yida-skills/skills/yida-publish-page/SKILL.md +113 -0
  44. package/.eslintrc.json +0 -25
  45. package/.github/workflows/ci.yml +0 -123
  46. package/.github/workflows/publish.yml +0 -105
  47. package/.github/workflows/update-contributors.yml +0 -151
  48. package/.openclaw/skills/yida-issue/SKILL.md +0 -27
  49. package/.openclaw/skills/yida-issue/scripts/create-issue.js +0 -317
  50. package/CLAUDE.md +0 -168
  51. package/CONTRIBUTING.md +0 -59
  52. package/install-skills.ps1 +0 -162
  53. package/install-skills.sh +0 -175
  54. package/pages/dist/.gitkeep +0 -0
  55. package/pages/src/.gitkeep +0 -0
  56. package/prd/salary-calculator.md +0 -15
  57. package/tests/cli.test.js +0 -930
  58. package/tests/install.test.js +0 -277
  59. package/tests/yida-issue.test.js +0 -314
  60. /package/{config.json → project/config.json} +0 -0
  61. /package/{.cache → project/pages/dist}/.gitkeep +0 -0
package/lib/utils.js ADDED
@@ -0,0 +1,443 @@
1
+ /**
2
+ * utils.js - 宜搭 CLI 公共工具函数
3
+ *
4
+ * 导出函数:
5
+ * findProjectRoot() - 查找项目根目录(兼容悟空环境)
6
+ * extractInfoFromCookies() - 从 Cookie 列表中提取 csrf_token / corp_id / user_id
7
+ * loadCookieData() - 读取 .cache/cookies.json 登录态缓存
8
+ * triggerLogin() - 触发登录(自动区分标准/悟空环境)
9
+ * refreshCsrfToken() - 刷新 csrf_token
10
+ * resolveBaseUrl() - 从 cookieData 中解析 base_url
11
+ * isLoginExpired() - 检测响应体是否表示登录过期
12
+ * isCsrfTokenExpired() - 检测响应体是否表示 csrf_token 过期
13
+ */
14
+
15
+ "use strict";
16
+
17
+ const fs = require("fs");
18
+ const path = require("path");
19
+ const os = require("os");
20
+ const { execSync } = require("child_process");
21
+
22
+ // ── 项目根目录查找 ────────────────────────────────────
23
+
24
+ /**
25
+ * 检测当前活跃的 AI 工具。
26
+ * 优先级:环境变量 > 兜底检测
27
+ *
28
+ * 注意:只返回当前"活跃"的工具,不返回已安装但未使用的工具。
29
+ *
30
+ * @returns {{ tool: string, displayName: string, dirName: string, workspaceRoot: string }|null}
31
+ */
32
+ function detectActiveTool() {
33
+ const env = process.env;
34
+ const cwd = process.cwd();
35
+ const home = os.homedir();
36
+
37
+ // 优先级1:通过环境变量检测
38
+
39
+ // Qoder
40
+ if (env.QODER_IDE || env.QODER_AGENT) {
41
+ return {
42
+ tool: "qoder",
43
+ displayName: "Qoder",
44
+ dirName: ".qoder",
45
+ workspaceRoot: path.join(cwd, "project"),
46
+ };
47
+ }
48
+
49
+ // Claude Code
50
+ if (env.CLAUDE_CODE) {
51
+ return {
52
+ tool: "claude-code",
53
+ displayName: "Claude Code",
54
+ dirName: ".claudecode",
55
+ workspaceRoot: path.join(cwd, "project"),
56
+ };
57
+ }
58
+
59
+ // OpenCode
60
+ if (env.OPENCODE) {
61
+ return {
62
+ tool: "opencode",
63
+ displayName: "OpenCode",
64
+ dirName: ".opencode",
65
+ workspaceRoot: path.join(cwd, "project"),
66
+ };
67
+ }
68
+
69
+ // Cursor
70
+ if (env.CURSOR_TRACE_ID || (env.VSCODE_GIT_ASKPASS_NODE || "").includes("Cursor")) {
71
+ return {
72
+ tool: "cursor",
73
+ displayName: "Cursor",
74
+ dirName: ".cursor",
75
+ workspaceRoot: path.join(cwd, "project"),
76
+ };
77
+ }
78
+
79
+ // iFlow
80
+ if (env.IFLOW_ACTIVE || env.IFLOW_IDE) {
81
+ return {
82
+ tool: "iflow",
83
+ displayName: "iFlow",
84
+ dirName: ".iflow",
85
+ workspaceRoot: path.join(cwd, "project"),
86
+ };
87
+ }
88
+
89
+ // 悟空(Wukong)
90
+ if (env.AGENT_WORK_ROOT && env.AGENT_WORK_ROOT.includes(".real")) {
91
+ return {
92
+ tool: "wukong",
93
+ displayName: "悟空(Wukong)",
94
+ dirName: ".real",
95
+ workspaceRoot: path.join(home, ".real", "workspace", "project"),
96
+ };
97
+ }
98
+
99
+ // 优先级2:兜底检测
100
+
101
+ // Aone Copilot - 通过专属配置目录检测(VSCode 环境)
102
+ // Aone Copilot 没有独立的环境变量,但会在 home 目录创建 ~/.aone_copilot/
103
+ if (env.TERM_PROGRAM === "vscode" && fs.existsSync(path.join(home, ".aone_copilot"))) {
104
+ return {
105
+ tool: "aone-copilot",
106
+ displayName: "Aone Copilot",
107
+ dirName: ".aone_copilot",
108
+ workspaceRoot: path.join(cwd, "project"),
109
+ };
110
+ }
111
+
112
+ // 未检测到活跃工具
113
+ return null;
114
+ }
115
+
116
+ /**
117
+ * 查找项目根目录(project 工作区)。
118
+ *
119
+ * 查找策略:
120
+ * 1. 通过环境变量检测当前活跃的 AI 工具
121
+ * 2. 返回对应工具的项目根目录
122
+ * 3. 兜底:返回 process.cwd()
123
+ *
124
+ * @returns {string} 项目根目录的绝对路径
125
+ */
126
+ function findProjectRoot() {
127
+ const activeTool = detectActiveTool();
128
+
129
+ if (activeTool) {
130
+ // 如果 project 目录存在,返回它;否则返回当前工作目录
131
+ if (fs.existsSync(activeTool.workspaceRoot)) {
132
+ return activeTool.workspaceRoot;
133
+ }
134
+ }
135
+
136
+ // 兜底:返回当前工作目录
137
+ return process.cwd();
138
+ }
139
+
140
+ // ── Cookie 解析 ───────────────────────────────────────
141
+
142
+ /**
143
+ * 从 Cookie 列表中提取 csrf_token、corp_id、user_id。
144
+ * @param {Array} cookies
145
+ * @returns {{ csrfToken: string|null, corpId: string|null, userId: string|null }}
146
+ */
147
+ function extractInfoFromCookies(cookies) {
148
+ let csrfToken = null;
149
+ let corpId = null;
150
+ let userId = null;
151
+
152
+ for (const cookie of cookies) {
153
+ if (cookie.name === "tianshu_csrf_token") {
154
+ csrfToken = cookie.value;
155
+ } else if (cookie.name === "tianshu_corp_user") {
156
+ const lastUnderscore = cookie.value.lastIndexOf("_");
157
+ if (lastUnderscore > 0) {
158
+ corpId = cookie.value.slice(0, lastUnderscore);
159
+ userId = cookie.value.slice(lastUnderscore + 1);
160
+ }
161
+ }
162
+ }
163
+
164
+ return { csrfToken, corpId, userId };
165
+ }
166
+
167
+ // ── 登录态缓存读取 ────────────────────────────────────
168
+
169
+ /**
170
+ * 读取 .cache/cookies.json 登录态缓存。
171
+ * @param {string} [projectRoot]
172
+ * @param {string} [defaultBaseUrl]
173
+ * @returns {object|null}
174
+ */
175
+ function loadCookieData(projectRoot, defaultBaseUrl) {
176
+ const root = projectRoot || findProjectRoot();
177
+ const fallbackBaseUrl = defaultBaseUrl || "https://www.aliwork.com";
178
+ const cookieFile = path.join(root, ".cache", "cookies.json");
179
+
180
+ if (!fs.existsSync(cookieFile)) return null;
181
+
182
+ try {
183
+ const raw = fs.readFileSync(cookieFile, "utf-8").trim();
184
+ if (!raw) return null;
185
+
186
+ const parsed = JSON.parse(raw);
187
+ let cookieData;
188
+
189
+ if (Array.isArray(parsed)) {
190
+ cookieData = { cookies: parsed, base_url: fallbackBaseUrl };
191
+ } else {
192
+ cookieData = parsed;
193
+ }
194
+
195
+ if (cookieData.cookies && cookieData.cookies.length > 0) {
196
+ const { csrfToken, corpId, userId } = extractInfoFromCookies(cookieData.cookies);
197
+ if (csrfToken) cookieData.csrf_token = csrfToken;
198
+ if (corpId) cookieData.corp_id = corpId;
199
+ if (userId) cookieData.user_id = userId;
200
+ }
201
+
202
+ return cookieData;
203
+ } catch {
204
+ return null;
205
+ }
206
+ }
207
+
208
+ // ── 登录触发 ──────────────────────────────────────────
209
+
210
+ /**
211
+ * 触发登录(Playwright 扫码模式)。
212
+ * @returns {object} loginResult
213
+ */
214
+ function triggerLogin() {
215
+ console.error("\n🔐 登录态失效,正在打开浏览器扫码登录...\n");
216
+ const { ensureLogin } = require("./login");
217
+ return ensureLogin();
218
+ }
219
+
220
+ /**
221
+ * 刷新 csrf_token(从本地缓存重新提取,无需重新扫码)。
222
+ * @returns {object} loginResult
223
+ */
224
+ function refreshCsrfToken() {
225
+ console.error("\n🔄 csrf_token 已过期,正在从 Cookie 重新提取...\n");
226
+ const { refreshCsrfFromCache } = require("./login");
227
+ return refreshCsrfFromCache();
228
+ }
229
+
230
+ // ── 响应检测 ──────────────────────────────────────────
231
+
232
+ /**
233
+ * 检测响应体是否表示登录过期。
234
+ * @param {object} responseJson
235
+ * @returns {boolean}
236
+ */
237
+ function isLoginExpired(responseJson) {
238
+ return (
239
+ responseJson &&
240
+ responseJson.success === false &&
241
+ (responseJson.errorCode === "307" || responseJson.errorCode === "302")
242
+ );
243
+ }
244
+
245
+ /**
246
+ * 检测响应体是否表示 csrf_token 过期。
247
+ * @param {object} responseJson
248
+ * @returns {boolean}
249
+ */
250
+ function isCsrfTokenExpired(responseJson) {
251
+ return (
252
+ responseJson &&
253
+ responseJson.success === false &&
254
+ responseJson.errorCode === "TIANSHU_000030"
255
+ );
256
+ }
257
+
258
+ // ── base_url 解析 ─────────────────────────────────────
259
+
260
+ /**
261
+ * 从 cookieData 中解析 base_url,去除末尾斜杠。
262
+ * @param {object} cookieData
263
+ * @param {string} [defaultBaseUrl]
264
+ * @returns {string}
265
+ */
266
+ function resolveBaseUrl(cookieData, defaultBaseUrl) {
267
+ const fallback = defaultBaseUrl || "https://www.aliwork.com";
268
+ return ((cookieData && cookieData.base_url) || fallback).replace(/\/+$/, "");
269
+ }
270
+
271
+ // ── HTTP 请求工具 ─────────────────────────────────────
272
+
273
+ /**
274
+ * 发送 HTTP POST 请求(application/x-www-form-urlencoded)
275
+ * @param {string} baseUrl
276
+ * @param {string} requestPath
277
+ * @param {string} postData - querystring 格式
278
+ * @param {Array} cookies
279
+ * @returns {Promise<object>}
280
+ */
281
+ function httpPost(baseUrl, requestPath, postData, cookies) {
282
+ const https = require("https");
283
+ const http = require("http");
284
+
285
+ return new Promise((resolve, reject) => {
286
+ const cookieHeader = cookies.map((c) => `${c.name}=${c.value}`).join("; ");
287
+ const parsedUrl = new URL(baseUrl);
288
+ const isHttps = parsedUrl.protocol === "https:";
289
+ const requestModule = isHttps ? https : http;
290
+
291
+ const options = {
292
+ hostname: parsedUrl.hostname,
293
+ port: parsedUrl.port || (isHttps ? 443 : 80),
294
+ path: requestPath,
295
+ method: "POST",
296
+ headers: {
297
+ "Content-Type": "application/x-www-form-urlencoded",
298
+ "Content-Length": Buffer.byteLength(postData),
299
+ Origin: baseUrl,
300
+ Referer: baseUrl + "/",
301
+ Cookie: cookieHeader,
302
+ },
303
+ timeout: 30000,
304
+ };
305
+
306
+ const req = requestModule.request(options, (res) => {
307
+ let data = "";
308
+ res.on("data", (chunk) => { data += chunk; });
309
+ res.on("end", () => {
310
+ console.error(` HTTP 状态码: ${res.statusCode}`);
311
+ try {
312
+ const parsed = JSON.parse(data);
313
+ if (isLoginExpired(parsed)) {
314
+ resolve({ __needLogin: true });
315
+ return;
316
+ }
317
+ if (isCsrfTokenExpired(parsed)) {
318
+ resolve({ __csrfExpired: true });
319
+ return;
320
+ }
321
+ resolve(parsed);
322
+ } catch {
323
+ console.error(` 响应内容: ${data.substring(0, 500)}`);
324
+ resolve({ success: false, errorMsg: `HTTP ${res.statusCode}: 响应非 JSON` });
325
+ }
326
+ });
327
+ });
328
+
329
+ req.on("timeout", () => { req.destroy(); reject(new Error("请求超时")); });
330
+ req.on("error", reject);
331
+ req.write(postData);
332
+ req.end();
333
+ });
334
+ }
335
+
336
+ /**
337
+ * 发送 HTTP GET 请求
338
+ * @param {string} baseUrl
339
+ * @param {string} requestPath
340
+ * @param {object} queryParams
341
+ * @param {Array} cookies
342
+ * @returns {Promise<object>}
343
+ */
344
+ function httpGet(baseUrl, requestPath, queryParams, cookies) {
345
+ const https = require("https");
346
+ const http = require("http");
347
+ const querystring = require("querystring");
348
+
349
+ return new Promise((resolve, reject) => {
350
+ const cookieHeader = cookies.map((c) => `${c.name}=${c.value}`).join("; ");
351
+ const parsedUrl = new URL(baseUrl);
352
+ const isHttps = parsedUrl.protocol === "https:";
353
+ const requestModule = isHttps ? https : http;
354
+ const fullPath = queryParams ? `${requestPath}?${querystring.stringify(queryParams)}` : requestPath;
355
+
356
+ const options = {
357
+ hostname: parsedUrl.hostname,
358
+ port: parsedUrl.port || (isHttps ? 443 : 80),
359
+ path: fullPath,
360
+ method: "GET",
361
+ headers: {
362
+ Origin: baseUrl,
363
+ Referer: baseUrl + "/",
364
+ Cookie: cookieHeader,
365
+ },
366
+ timeout: 30000,
367
+ };
368
+
369
+ const req = requestModule.request(options, (res) => {
370
+ let data = "";
371
+ res.on("data", (chunk) => { data += chunk; });
372
+ res.on("end", () => {
373
+ console.error(` HTTP 状态码: ${res.statusCode}`);
374
+ try {
375
+ const parsed = JSON.parse(data);
376
+ if (isLoginExpired(parsed)) {
377
+ resolve({ __needLogin: true });
378
+ return;
379
+ }
380
+ if (isCsrfTokenExpired(parsed)) {
381
+ resolve({ __csrfExpired: true });
382
+ return;
383
+ }
384
+ resolve(parsed);
385
+ } catch {
386
+ console.error(` 响应内容: ${data.substring(0, 500)}`);
387
+ resolve({ success: false, errorMsg: `HTTP ${res.statusCode}: 响应非 JSON` });
388
+ }
389
+ });
390
+ });
391
+
392
+ req.on("timeout", () => { req.destroy(); reject(new Error("请求超时")); });
393
+ req.on("error", reject);
394
+ req.end();
395
+ });
396
+ }
397
+
398
+ /**
399
+ * 带自动重登录的请求封装。
400
+ * @param {Function} requestFn - 接受 authRef 返回 Promise 的工厂函数
401
+ * @param {object} authRef - { csrfToken, cookies, baseUrl, cookieData }
402
+ * @returns {Promise<object>}
403
+ */
404
+ async function requestWithAutoLogin(requestFn, authRef) {
405
+ let result = await requestFn(authRef);
406
+
407
+ if (result && result.__csrfExpired) {
408
+ const refreshedData = refreshCsrfToken();
409
+ authRef.cookieData = refreshedData;
410
+ authRef.csrfToken = refreshedData.csrf_token;
411
+ authRef.cookies = refreshedData.cookies;
412
+ authRef.baseUrl = resolveBaseUrl(refreshedData);
413
+ console.error(" 🔄 csrf_token 已刷新,重试...");
414
+ result = await requestFn(authRef);
415
+ }
416
+
417
+ if (result && result.__needLogin) {
418
+ const newCookieData = triggerLogin();
419
+ authRef.cookieData = newCookieData;
420
+ authRef.csrfToken = newCookieData.csrf_token;
421
+ authRef.cookies = newCookieData.cookies;
422
+ authRef.baseUrl = resolveBaseUrl(newCookieData);
423
+ console.error(" 🔄 重新登录后重试...");
424
+ result = await requestFn(authRef);
425
+ }
426
+
427
+ return result;
428
+ }
429
+
430
+ module.exports = {
431
+ detectActiveTool,
432
+ findProjectRoot,
433
+ extractInfoFromCookies,
434
+ loadCookieData,
435
+ triggerLogin,
436
+ refreshCsrfToken,
437
+ resolveBaseUrl,
438
+ isLoginExpired,
439
+ isCsrfTokenExpired,
440
+ httpPost,
441
+ httpGet,
442
+ requestWithAutoLogin,
443
+ };