@jackwener/opencli 1.5.4 → 1.5.6

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 (256) hide show
  1. package/README.md +27 -2
  2. package/README.zh-CN.md +36 -4
  3. package/dist/browser/daemon-client.d.ts +5 -1
  4. package/dist/browser/page.d.ts +6 -0
  5. package/dist/browser/page.js +15 -0
  6. package/dist/cli-manifest.json +1284 -67
  7. package/dist/cli.js +14 -14
  8. package/dist/clis/antigravity/serve.js +2 -2
  9. package/dist/clis/band/bands.d.ts +1 -0
  10. package/dist/clis/band/bands.js +72 -0
  11. package/dist/clis/band/mentions.d.ts +1 -0
  12. package/dist/clis/band/mentions.js +127 -0
  13. package/dist/clis/band/post.d.ts +1 -0
  14. package/dist/clis/band/post.js +175 -0
  15. package/dist/clis/band/posts.d.ts +1 -0
  16. package/dist/clis/band/posts.js +94 -0
  17. package/dist/clis/doubao/detail.d.ts +1 -0
  18. package/dist/clis/doubao/detail.js +33 -0
  19. package/dist/clis/doubao/detail.test.d.ts +1 -0
  20. package/dist/clis/doubao/detail.test.js +42 -0
  21. package/dist/clis/doubao/history.d.ts +1 -0
  22. package/dist/clis/doubao/history.js +28 -0
  23. package/dist/clis/doubao/history.test.d.ts +1 -0
  24. package/dist/clis/doubao/history.test.js +37 -0
  25. package/dist/clis/doubao/meeting-summary.d.ts +1 -0
  26. package/dist/clis/doubao/meeting-summary.js +39 -0
  27. package/dist/clis/doubao/meeting-transcript.d.ts +1 -0
  28. package/dist/clis/doubao/meeting-transcript.js +36 -0
  29. package/dist/clis/doubao/utils.d.ts +27 -0
  30. package/dist/clis/doubao/utils.js +317 -0
  31. package/dist/clis/doubao/utils.test.d.ts +1 -0
  32. package/dist/clis/doubao/utils.test.js +24 -0
  33. package/dist/clis/douyin/_shared/public-api.d.ts +33 -0
  34. package/dist/clis/douyin/_shared/public-api.js +29 -0
  35. package/dist/clis/douyin/user-videos.d.ts +5 -0
  36. package/dist/clis/douyin/user-videos.js +74 -0
  37. package/dist/clis/douyin/user-videos.test.d.ts +1 -0
  38. package/dist/clis/douyin/user-videos.test.js +108 -0
  39. package/dist/clis/ones/common.d.ts +32 -0
  40. package/dist/clis/ones/common.js +144 -0
  41. package/dist/clis/ones/enrich-tasks.d.ts +5 -0
  42. package/dist/clis/ones/enrich-tasks.js +37 -0
  43. package/dist/clis/ones/login.d.ts +1 -0
  44. package/dist/clis/ones/login.js +80 -0
  45. package/dist/clis/ones/logout.d.ts +1 -0
  46. package/dist/clis/ones/logout.js +17 -0
  47. package/dist/clis/ones/me.d.ts +1 -0
  48. package/dist/clis/ones/me.js +30 -0
  49. package/dist/clis/ones/my-tasks.d.ts +1 -0
  50. package/dist/clis/ones/my-tasks.js +120 -0
  51. package/dist/clis/ones/resolve-labels.d.ts +10 -0
  52. package/dist/clis/ones/resolve-labels.js +64 -0
  53. package/dist/clis/ones/task-helpers.d.ts +29 -0
  54. package/dist/clis/ones/task-helpers.js +212 -0
  55. package/dist/clis/ones/task-helpers.test.d.ts +1 -0
  56. package/dist/clis/ones/task-helpers.test.js +12 -0
  57. package/dist/clis/ones/task.d.ts +1 -0
  58. package/dist/clis/ones/task.js +66 -0
  59. package/dist/clis/ones/tasks.d.ts +1 -0
  60. package/dist/clis/ones/tasks.js +79 -0
  61. package/dist/clis/ones/token-info.d.ts +1 -0
  62. package/dist/clis/ones/token-info.js +42 -0
  63. package/dist/clis/ones/worklog.d.ts +11 -0
  64. package/dist/clis/ones/worklog.js +267 -0
  65. package/dist/clis/ones/worklog.test.d.ts +1 -0
  66. package/dist/clis/ones/worklog.test.js +20 -0
  67. package/dist/clis/sinafinance/rolling-news.d.ts +4 -0
  68. package/dist/clis/sinafinance/rolling-news.js +40 -0
  69. package/dist/clis/sinafinance/stock.d.ts +8 -0
  70. package/dist/clis/sinafinance/stock.js +117 -0
  71. package/dist/clis/spotify/spotify.d.ts +1 -0
  72. package/dist/clis/spotify/spotify.js +316 -0
  73. package/dist/clis/spotify/utils.d.ts +21 -0
  74. package/dist/clis/spotify/utils.js +66 -0
  75. package/dist/clis/spotify/utils.test.d.ts +1 -0
  76. package/dist/clis/spotify/utils.test.js +67 -0
  77. package/dist/clis/tieba/commands.test.d.ts +4 -0
  78. package/dist/clis/tieba/commands.test.js +79 -0
  79. package/dist/clis/tieba/hot.d.ts +1 -0
  80. package/dist/clis/tieba/hot.js +48 -0
  81. package/dist/clis/tieba/posts.d.ts +1 -0
  82. package/dist/clis/tieba/posts.js +85 -0
  83. package/dist/clis/tieba/read.d.ts +1 -0
  84. package/dist/clis/tieba/read.js +140 -0
  85. package/dist/clis/tieba/search.d.ts +1 -0
  86. package/dist/clis/tieba/search.js +108 -0
  87. package/dist/clis/tieba/utils.d.ts +101 -0
  88. package/dist/clis/tieba/utils.js +240 -0
  89. package/dist/clis/tieba/utils.test.d.ts +1 -0
  90. package/dist/clis/tieba/utils.test.js +290 -0
  91. package/dist/clis/weread/book.js +100 -13
  92. package/dist/clis/weread/commands.test.js +221 -0
  93. package/dist/clis/weread/private-api-regression.test.d.ts +1 -0
  94. package/dist/{weread-private-api-regression.test.js → clis/weread/private-api-regression.test.js} +92 -30
  95. package/dist/clis/weread/search-regression.test.d.ts +1 -0
  96. package/dist/clis/weread/search-regression.test.js +407 -0
  97. package/dist/clis/weread/search.js +143 -7
  98. package/dist/clis/weread/shelf.js +13 -95
  99. package/dist/clis/weread/utils.d.ts +46 -0
  100. package/dist/clis/weread/utils.js +214 -7
  101. package/dist/clis/weread/utils.test.js +71 -1
  102. package/dist/clis/xiaohongshu/publish.d.ts +1 -1
  103. package/dist/clis/xiaohongshu/publish.js +78 -31
  104. package/dist/clis/xiaohongshu/publish.test.js +66 -1
  105. package/dist/clis/xiaohongshu/user-helpers.d.ts +1 -0
  106. package/dist/clis/xiaohongshu/user-helpers.js +2 -0
  107. package/dist/clis/xiaohongshu/user-helpers.test.js +18 -0
  108. package/dist/clis/xueqiu/comments.d.ts +118 -0
  109. package/dist/clis/xueqiu/comments.js +354 -0
  110. package/dist/clis/xueqiu/comments.test.d.ts +1 -0
  111. package/dist/clis/xueqiu/comments.test.js +696 -0
  112. package/dist/clis/youtube/transcript.js +2 -4
  113. package/dist/clis/youtube/utils.d.ts +9 -0
  114. package/dist/clis/youtube/utils.js +67 -3
  115. package/dist/clis/youtube/utils.test.d.ts +1 -0
  116. package/dist/clis/youtube/utils.test.js +37 -0
  117. package/dist/clis/youtube/video.js +16 -15
  118. package/dist/clis/zsxq/dynamics.d.ts +1 -0
  119. package/dist/clis/zsxq/dynamics.js +47 -0
  120. package/dist/clis/zsxq/groups.d.ts +1 -0
  121. package/dist/clis/zsxq/groups.js +32 -0
  122. package/dist/clis/zsxq/search.d.ts +1 -0
  123. package/dist/clis/zsxq/search.js +43 -0
  124. package/dist/clis/zsxq/search.test.d.ts +1 -0
  125. package/dist/clis/zsxq/search.test.js +24 -0
  126. package/dist/clis/zsxq/topic.d.ts +1 -0
  127. package/dist/clis/zsxq/topic.js +47 -0
  128. package/dist/clis/zsxq/topic.test.d.ts +1 -0
  129. package/dist/clis/zsxq/topic.test.js +29 -0
  130. package/dist/clis/zsxq/topics.d.ts +1 -0
  131. package/dist/clis/zsxq/topics.js +25 -0
  132. package/dist/clis/zsxq/topics.test.d.ts +1 -0
  133. package/dist/clis/zsxq/topics.test.js +24 -0
  134. package/dist/clis/zsxq/utils.d.ts +97 -0
  135. package/dist/clis/zsxq/utils.js +230 -0
  136. package/dist/commanderAdapter.js +27 -4
  137. package/dist/commanderAdapter.test.js +39 -0
  138. package/dist/daemon.js +5 -4
  139. package/dist/errors.d.ts +29 -1
  140. package/dist/errors.js +49 -11
  141. package/dist/external-clis.yaml +17 -0
  142. package/dist/external.js +3 -3
  143. package/dist/main.js +2 -1
  144. package/dist/tui.js +2 -1
  145. package/dist/types.d.ts +5 -0
  146. package/docs/.vitepress/config.mts +3 -0
  147. package/docs/adapters/browser/band.md +63 -0
  148. package/docs/adapters/browser/ones.md +59 -0
  149. package/docs/adapters/browser/sinafinance.md +56 -6
  150. package/docs/adapters/browser/spotify.md +62 -0
  151. package/docs/adapters/browser/tieba.md +45 -0
  152. package/docs/adapters/browser/xueqiu.md +5 -0
  153. package/docs/adapters/browser/zsxq.md +49 -0
  154. package/docs/adapters/index.md +5 -2
  155. package/docs/adapters-doc/ones.md +32 -0
  156. package/extension/dist/background.js +1 -2
  157. package/extension/manifest.json +1 -1
  158. package/extension/package.json +1 -1
  159. package/extension/src/background.ts +17 -1
  160. package/extension/src/cdp.ts +42 -0
  161. package/extension/src/protocol.ts +5 -1
  162. package/package.json +1 -1
  163. package/scripts/postinstall.js +16 -0
  164. package/src/browser/daemon-client.ts +5 -1
  165. package/src/browser/page.ts +16 -0
  166. package/src/cli.ts +14 -14
  167. package/src/clis/antigravity/serve.ts +2 -2
  168. package/src/clis/band/bands.ts +76 -0
  169. package/src/clis/band/mentions.ts +134 -0
  170. package/src/clis/band/post.ts +187 -0
  171. package/src/clis/band/posts.ts +106 -0
  172. package/src/clis/doubao/detail.test.ts +53 -0
  173. package/src/clis/doubao/detail.ts +41 -0
  174. package/src/clis/doubao/history.test.ts +45 -0
  175. package/src/clis/doubao/history.ts +32 -0
  176. package/src/clis/doubao/meeting-summary.ts +53 -0
  177. package/src/clis/doubao/meeting-transcript.ts +48 -0
  178. package/src/clis/doubao/utils.test.ts +45 -0
  179. package/src/clis/doubao/utils.ts +371 -0
  180. package/src/clis/douyin/_shared/public-api.ts +84 -0
  181. package/src/clis/douyin/user-videos.test.ts +122 -0
  182. package/src/clis/douyin/user-videos.ts +101 -0
  183. package/src/clis/ones/common.ts +187 -0
  184. package/src/clis/ones/enrich-tasks.ts +47 -0
  185. package/src/clis/ones/login.ts +103 -0
  186. package/src/clis/ones/logout.ts +19 -0
  187. package/src/clis/ones/me.ts +34 -0
  188. package/src/clis/ones/my-tasks.ts +148 -0
  189. package/src/clis/ones/resolve-labels.ts +80 -0
  190. package/src/clis/ones/task-helpers.test.ts +14 -0
  191. package/src/clis/ones/task-helpers.ts +214 -0
  192. package/src/clis/ones/task.ts +79 -0
  193. package/src/clis/ones/tasks.ts +92 -0
  194. package/src/clis/ones/token-info.ts +46 -0
  195. package/src/clis/ones/worklog.test.ts +24 -0
  196. package/src/clis/ones/worklog.ts +306 -0
  197. package/src/clis/sinafinance/rolling-news.ts +42 -0
  198. package/src/clis/sinafinance/stock.ts +127 -0
  199. package/src/clis/spotify/spotify.ts +328 -0
  200. package/src/clis/spotify/utils.test.ts +87 -0
  201. package/src/clis/spotify/utils.ts +92 -0
  202. package/src/clis/tieba/commands.test.ts +86 -0
  203. package/src/clis/tieba/hot.ts +52 -0
  204. package/src/clis/tieba/posts.ts +108 -0
  205. package/src/clis/tieba/read.ts +158 -0
  206. package/src/clis/tieba/search.ts +119 -0
  207. package/src/clis/tieba/utils.test.ts +322 -0
  208. package/src/clis/tieba/utils.ts +348 -0
  209. package/src/clis/weread/book.ts +116 -13
  210. package/src/clis/weread/commands.test.ts +249 -0
  211. package/src/{weread-private-api-regression.test.ts → clis/weread/private-api-regression.test.ts} +108 -30
  212. package/src/clis/weread/search-regression.test.ts +440 -0
  213. package/src/clis/weread/search.ts +189 -9
  214. package/src/clis/weread/shelf.ts +20 -122
  215. package/src/clis/weread/utils.test.ts +81 -1
  216. package/src/clis/weread/utils.ts +264 -7
  217. package/src/clis/xiaohongshu/publish.test.ts +79 -1
  218. package/src/clis/xiaohongshu/publish.ts +84 -30
  219. package/src/clis/xiaohongshu/user-helpers.test.ts +23 -0
  220. package/src/clis/xiaohongshu/user-helpers.ts +4 -0
  221. package/src/clis/xueqiu/comments.test.ts +823 -0
  222. package/src/clis/xueqiu/comments.ts +461 -0
  223. package/src/clis/youtube/transcript.ts +2 -4
  224. package/src/clis/youtube/utils.test.ts +43 -0
  225. package/src/clis/youtube/utils.ts +69 -0
  226. package/src/clis/youtube/video.ts +16 -15
  227. package/src/clis/zsxq/dynamics.ts +60 -0
  228. package/src/clis/zsxq/groups.ts +41 -0
  229. package/src/clis/zsxq/search.test.ts +29 -0
  230. package/src/clis/zsxq/search.ts +54 -0
  231. package/src/clis/zsxq/topic.test.ts +34 -0
  232. package/src/clis/zsxq/topic.ts +68 -0
  233. package/src/clis/zsxq/topics.test.ts +29 -0
  234. package/src/clis/zsxq/topics.ts +36 -0
  235. package/src/clis/zsxq/utils.ts +351 -0
  236. package/src/commanderAdapter.test.ts +47 -0
  237. package/src/commanderAdapter.ts +26 -3
  238. package/src/daemon.ts +5 -4
  239. package/src/errors.ts +71 -10
  240. package/src/external-clis.yaml +17 -0
  241. package/src/external.ts +3 -3
  242. package/src/main.ts +2 -1
  243. package/src/tui.ts +2 -1
  244. package/src/types.ts +5 -0
  245. package/tests/e2e/band-auth.test.ts +20 -0
  246. package/tests/e2e/browser-auth-helpers.ts +18 -0
  247. package/tests/e2e/browser-auth.test.ts +35 -47
  248. package/tests/e2e/browser-public.test.ts +288 -0
  249. package/tests/e2e/management.test.ts +1 -1
  250. package/tests/e2e/plugin-management.test.ts +1 -1
  251. package/vitest.config.ts +1 -0
  252. package/SKILL.md +0 -879
  253. package/dist/weread-private-api-regression.test.d.ts +0 -1
  254. package/dist/weread-search-regression.test.d.ts +0 -1
  255. package/dist/weread-search-regression.test.js +0 -39
  256. package/src/weread-search-regression.test.ts +0 -44
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Log/backfill work hours. Project API paths vary by deployment,
3
+ * so we try common endpoints in sequence.
4
+ */
5
+
6
+ import { cli, Strategy } from '../../registry.js';
7
+ import { CliError } from '../../errors.js';
8
+ import {
9
+ gotoOnesHome,
10
+ onesFetchInPageWithMeta,
11
+ resolveOnesUserUuid,
12
+ summarizeOnesError,
13
+ } from './common.js';
14
+ import { hoursToOnesManhourRaw } from './task-helpers.js';
15
+
16
+ function summarizeOnesMutationBody(parsed: unknown, status: number): string | null {
17
+ if (!parsed || typeof parsed !== 'object') {
18
+ return status >= 400 ? `HTTP ${status}` : null;
19
+ }
20
+ const o = parsed as Record<string, unknown>;
21
+ if (Array.isArray(o.errors) && o.errors.length > 0) {
22
+ const e0 = o.errors[0];
23
+ if (e0 && typeof e0 === 'object') {
24
+ const msg = String((e0 as Record<string, unknown>).message ?? '').trim();
25
+ if (msg) return msg;
26
+ }
27
+ return 'graphql errors';
28
+ }
29
+ if (o.data && typeof o.data === 'object') {
30
+ const data = o.data as Record<string, unknown>;
31
+ if (data.addManhour && typeof data.addManhour === 'object') {
32
+ const key = String((data.addManhour as Record<string, unknown>).key ?? '').trim();
33
+ if (!key) return 'addManhour returned empty key';
34
+ }
35
+ }
36
+ if (Array.isArray(o.bad_tasks) && o.bad_tasks.length > 0) {
37
+ const b = o.bad_tasks[0] as Record<string, unknown>;
38
+ return String(b.desc ?? b.code ?? JSON.stringify(b));
39
+ }
40
+ if (typeof o.reason === 'string' && o.reason.trim()) return o.reason.trim();
41
+ const c = o.code;
42
+ if (c !== undefined && c !== null) {
43
+ const n = Number(c);
44
+ if (Number.isFinite(n) && n !== 200 && n !== 0) return `code=${String(c)}`;
45
+ }
46
+ const ec = o.errcode;
47
+ if (typeof ec === 'string' && ec && ec !== 'OK') return ec;
48
+ return null;
49
+ }
50
+
51
+ function describeAttemptFailure(r: { ok: boolean; status: number; parsed: unknown }): string | null {
52
+ if (!r.ok) return summarizeOnesError(r.status, r.parsed);
53
+ return summarizeOnesMutationBody(r.parsed, r.status);
54
+ }
55
+
56
+ function todayLocalYmd(): string {
57
+ const d = new Date();
58
+ const y = d.getFullYear();
59
+ const m = String(d.getMonth() + 1).padStart(2, '0');
60
+ const day = String(d.getDate()).padStart(2, '0');
61
+ return `${y}-${m}-${day}`;
62
+ }
63
+
64
+ function validateYmd(s: string): boolean {
65
+ return /^\d{4}-\d{2}-\d{2}$/.test(s);
66
+ }
67
+
68
+ function toLocalMidnightUnixSeconds(ymd: string): number {
69
+ const d = new Date(`${ymd}T00:00:00`);
70
+ const ms = d.getTime();
71
+ if (!Number.isFinite(ms)) return 0;
72
+ return Math.floor(ms / 1000);
73
+ }
74
+
75
+ function pickTaskTotalManhourRaw(parsed: unknown): number | null {
76
+ if (!parsed || typeof parsed !== 'object') return null;
77
+ const o = parsed as Record<string, unknown>;
78
+ const n = Number(o.total_manhour);
79
+ return Number.isFinite(n) ? n : null;
80
+ }
81
+
82
+ export function buildAddManhourGraphqlBody(input: {
83
+ ownerId: string;
84
+ taskId: string;
85
+ startTime: number;
86
+ rawManhour: number;
87
+ note: string;
88
+ }): string {
89
+ const { ownerId, taskId, startTime, rawManhour, note } = input;
90
+ const description = JSON.stringify(note);
91
+ const owner = JSON.stringify(ownerId);
92
+ const task = JSON.stringify(taskId);
93
+
94
+ return JSON.stringify({
95
+ query: `mutation AddManhour {
96
+ addManhour(
97
+ mode: "simple"
98
+ owner: ${owner}
99
+ task: ${task}
100
+ type: "recorded"
101
+ start_time: ${startTime}
102
+ hours: ${rawManhour}
103
+ description: ${description}
104
+ customData: {}
105
+ ) {
106
+ key
107
+ }
108
+ }`,
109
+ });
110
+ }
111
+
112
+ cli({
113
+ site: 'ones',
114
+ name: 'worklog',
115
+ description:
116
+ 'ONES — log work hours on a task (defaults to today; use --date to backfill; endpoint falls back by deployment).',
117
+ domain: 'ones.cn',
118
+ strategy: Strategy.COOKIE,
119
+ browser: true,
120
+ navigateBefore: false,
121
+ args: [
122
+ {
123
+ name: 'task',
124
+ type: 'str',
125
+ required: true,
126
+ positional: true,
127
+ help: 'Work item UUID (usually 16 chars), from my-tasks or browser URL …/task/<id>',
128
+ },
129
+ {
130
+ name: 'hours',
131
+ type: 'str',
132
+ required: true,
133
+ positional: true,
134
+ help: 'Hours to log for this entry (e.g. 2 or 1.5), converted with ONES_MANHOUR_SCALE',
135
+ },
136
+ {
137
+ name: 'team',
138
+ type: 'str',
139
+ required: false,
140
+ help: 'Team UUID from URL …/team/<uuid>/…, or set ONES_TEAM_UUID',
141
+ },
142
+ {
143
+ name: 'date',
144
+ type: 'str',
145
+ required: false,
146
+ help: 'Entry date YYYY-MM-DD, defaults to today (local timezone); use for backfill',
147
+ },
148
+ {
149
+ name: 'note',
150
+ type: 'str',
151
+ required: false,
152
+ help: 'Optional note (written to description/desc)',
153
+ },
154
+ {
155
+ name: 'owner',
156
+ type: 'str',
157
+ required: false,
158
+ help: 'Owner user UUID (defaults to current logged-in user)',
159
+ },
160
+ ],
161
+ columns: ['task', 'date', 'hours', 'owner', 'endpoint'],
162
+
163
+ func: async (page, kwargs) => {
164
+ const taskId = String(kwargs.task ?? '').trim();
165
+ if (!taskId) {
166
+ throw new CliError('CONFIG', 'task uuid required', 'Pass the work item uuid from opencli ones my-tasks or the URL.');
167
+ }
168
+
169
+ const team =
170
+ (kwargs.team as string | undefined)?.trim() ||
171
+ process.env.ONES_TEAM_UUID?.trim() ||
172
+ process.env.ONES_TEAM_ID?.trim();
173
+ if (!team) {
174
+ throw new CliError(
175
+ 'CONFIG',
176
+ 'team UUID required',
177
+ 'Pass --team or set ONES_TEAM_UUID (from …/team/<team>/…).',
178
+ );
179
+ }
180
+
181
+ const hoursHuman = Number(String(kwargs.hours ?? '').replace(/,/g, ''));
182
+ if (!Number.isFinite(hoursHuman) || hoursHuman <= 0 || hoursHuman > 1000) {
183
+ throw new CliError(
184
+ 'CONFIG',
185
+ 'hours must be a positive number (hours)',
186
+ 'Example: opencli ones worklog <taskUUID> 2 --team <teamUUID>',
187
+ );
188
+ }
189
+
190
+ const dateArg = (kwargs.date as string | undefined)?.trim();
191
+ const dateStr = dateArg || todayLocalYmd();
192
+ if (!validateYmd(dateStr)) {
193
+ throw new CliError('CONFIG', 'invalid --date', 'Use YYYY-MM-DD, e.g. 2026-03-24.');
194
+ }
195
+
196
+ const note = String(kwargs.note ?? '').trim();
197
+ const rawManhour = hoursToOnesManhourRaw(hoursHuman);
198
+ const startTime = toLocalMidnightUnixSeconds(dateStr);
199
+ if (!startTime) {
200
+ throw new CliError('CONFIG', 'invalid date for start_time', `Could not parse date ${dateStr}.`);
201
+ }
202
+
203
+ await gotoOnesHome(page);
204
+
205
+ const ownerFromKw = (kwargs.owner as string | undefined)?.trim();
206
+ const ownerId = ownerFromKw || (await resolveOnesUserUuid(page, { skipGoto: true }));
207
+
208
+ const entry: Record<string, unknown> = {
209
+ owner: ownerId,
210
+ manhour: rawManhour,
211
+ start_date: dateStr,
212
+ end_date: dateStr,
213
+ desc: note,
214
+ };
215
+ const entryAlt: Record<string, unknown> = {
216
+ owner: ownerId,
217
+ allManhour: rawManhour,
218
+ startDate: dateStr,
219
+ endDate: dateStr,
220
+ desc: note,
221
+ };
222
+
223
+ const enc = encodeURIComponent(taskId);
224
+ const gqlBody = buildAddManhourGraphqlBody({
225
+ ownerId,
226
+ taskId,
227
+ startTime,
228
+ rawManhour,
229
+ note,
230
+ });
231
+ const attempts: { path: string; body: string }[] = [
232
+ { path: `team/${team}/items/graphql`, body: gqlBody },
233
+ { path: `team/${team}/task/${enc}/manhours/add`, body: JSON.stringify(entry) },
234
+ { path: `team/${team}/task/${enc}/manhours/add`, body: JSON.stringify(entryAlt) },
235
+ { path: `team/${team}/task/${enc}/manhours/add`, body: JSON.stringify({ manhours: [entry] }) },
236
+ { path: `team/${team}/task/${enc}/manhours/add`, body: JSON.stringify({ manhours: [entryAlt] }) },
237
+ { path: `team/${team}/task/${enc}/manhour/add`, body: JSON.stringify(entry) },
238
+ { path: `team/${team}/task/${enc}/manhour/add`, body: JSON.stringify(entryAlt) },
239
+ {
240
+ path: `team/${team}/tasks/update3`,
241
+ body: JSON.stringify({
242
+ tasks: [{ uuid: taskId, manhours: [entry] }],
243
+ }),
244
+ },
245
+ ];
246
+
247
+ const beforeInfo = await onesFetchInPageWithMeta(page, `team/${team}/task/${enc}/info`, {
248
+ method: 'GET',
249
+ skipGoto: true,
250
+ });
251
+ const beforeTotal = beforeInfo.ok ? pickTaskTotalManhourRaw(beforeInfo.parsed) : null;
252
+
253
+ let lastDetail = '';
254
+ for (const { path, body } of attempts) {
255
+ const r = await onesFetchInPageWithMeta(page, path, {
256
+ method: 'POST',
257
+ body,
258
+ skipGoto: true,
259
+ });
260
+ const fail = describeAttemptFailure(r);
261
+ if (!fail) {
262
+ // Guard against false success: HTTP 200 but no actual manhour change.
263
+ const afterInfo = await onesFetchInPageWithMeta(page, `team/${team}/task/${enc}/info`, {
264
+ method: 'GET',
265
+ skipGoto: true,
266
+ });
267
+ if (afterInfo.ok) {
268
+ const afterTotal = pickTaskTotalManhourRaw(afterInfo.parsed);
269
+ const changed =
270
+ beforeTotal === null
271
+ ? afterTotal !== null
272
+ : afterTotal !== null && Math.abs(afterTotal - beforeTotal) >= 1;
273
+ if (changed) {
274
+ return [
275
+ {
276
+ task: taskId,
277
+ date: dateStr,
278
+ hours: String(hoursHuman),
279
+ owner: ownerId,
280
+ endpoint: path,
281
+ },
282
+ ];
283
+ }
284
+ lastDetail = `no effect (total_manhour ${String(beforeTotal)} -> ${String(afterTotal)})`;
285
+ continue;
286
+ }
287
+ // If verification read fails, return success conservatively.
288
+ return [
289
+ {
290
+ task: taskId,
291
+ date: dateStr,
292
+ hours: String(hoursHuman),
293
+ owner: ownerId,
294
+ endpoint: path,
295
+ },
296
+ ];
297
+ }
298
+ lastDetail = fail;
299
+ }
300
+
301
+ throw new CliError(
302
+ 'FETCH_ERROR',
303
+ `ONES worklog: all endpoints failed (last: ${lastDetail})`,
304
+ );
305
+ },
306
+ });
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Sinafinance rolling news feed
3
+ */
4
+
5
+ import { cli, Strategy } from '../../registry.js';
6
+
7
+ cli({
8
+ site: 'sinafinance',
9
+ name: 'rolling-news',
10
+ description: '新浪财经滚动新闻',
11
+ domain: 'finance.sina.com.cn/roll',
12
+ strategy: Strategy.COOKIE,
13
+ args: [],
14
+ columns: ['column', 'title', 'date', 'url'],
15
+ func: async (page, _args) => {
16
+ await page.goto(`https://finance.sina.com.cn/roll/#pageid=384&lid=2519`);
17
+ await page.wait({ selector: '.d_list_txt li', timeout: 10000 });
18
+
19
+ const payload = await page.evaluate(`
20
+ (() => {
21
+ const cleanText = (value) => (value || '').replace(/\\s+/g, ' ').trim();
22
+ const results = [];
23
+ document.querySelectorAll('.d_list_txt li').forEach(el => {
24
+ const titleEl = el.querySelector('.c_tit a');
25
+ const columnEl = el.querySelector('.c_chl');
26
+ const dateEl = el.querySelector('.c_time');
27
+ const url = titleEl?.getAttribute('href') || '';
28
+ if (!url) return;
29
+ results.push({
30
+ title: cleanText(titleEl?.textContent || ''),
31
+ column: cleanText(columnEl?.textContent || ''),
32
+ date: cleanText(dateEl?.textContent || ''),
33
+ url: url,
34
+ });
35
+ });
36
+ return results;
37
+ })()
38
+ `);
39
+ if (!Array.isArray(payload)) return [];
40
+ return payload;
41
+ },
42
+ });
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Sinafinance stock quote — A股 / 港股 / 美股
3
+ *
4
+ * Uses two public Sina APIs (no browser required):
5
+ * suggest3.sinajs.cn — symbol search
6
+ * hq.sinajs.cn — real-time quote
7
+ */
8
+
9
+ import { cli, Strategy } from '../../registry.js';
10
+ import { CliError } from '../../errors.js';
11
+
12
+ const MARKET_CN = '11';
13
+ const MARKET_HK = '31';
14
+ const MARKET_US = '41';
15
+
16
+ async function fetchGBK(url: string): Promise<string> {
17
+ const res = await fetch(url, { headers: { Referer: 'https://finance.sina.com.cn' } });
18
+ if (!res.ok) throw new CliError('FETCH_ERROR', `Sina API HTTP ${res.status}`, 'Check your network');
19
+ const buf = await res.arrayBuffer();
20
+ return new TextDecoder('gbk').decode(buf);
21
+ }
22
+
23
+ interface SuggestEntry { name: string; market: string; symbol: string; }
24
+
25
+ function parseSuggest(raw: string, markets: string[]): SuggestEntry[] {
26
+ const m = raw.match(/suggestvalue="(.*)"/s);
27
+ if (!m) return [];
28
+ return m[1].split(';').filter(Boolean).map(s => {
29
+ const p = s.split(',');
30
+ return { name: p[4] || p[0] || '', market: p[1] || '', symbol: p[3] || '' };
31
+ }).filter(e => markets.includes(e.market));
32
+ }
33
+
34
+ function hqSymbol(e: SuggestEntry): string {
35
+ if (e.market === MARKET_HK) return `hk${e.symbol}`;
36
+ if (e.market === MARKET_US) return `gb_${e.symbol}`;
37
+ return e.symbol; // A股: already "sh600519" / "sz300XXX"
38
+ }
39
+
40
+ function parseHq(raw: string, sym: string): string[] {
41
+ const escaped = sym.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
42
+ const m = raw.match(new RegExp(`hq_str_${escaped}="([^"]*)"`));
43
+ return m ? m[1].split(',') : [];
44
+ }
45
+
46
+ function fmtMktCap(val: string): string {
47
+ const n = parseFloat(val);
48
+ if (!n) return '';
49
+ if (n >= 1e12) return (n / 1e12).toFixed(2) + 'T';
50
+ if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B';
51
+ if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M';
52
+ return String(n);
53
+ }
54
+
55
+ cli({
56
+ site: 'sinafinance',
57
+ name: 'stock',
58
+ description: '新浪财经行情(A股/港股/美股)',
59
+ domain: 'suggest3.sinajs.cn,hq.sinajs.cn',
60
+ strategy: Strategy.PUBLIC,
61
+ browser: false,
62
+ args: [
63
+ { name: 'key', type: 'string', required: true, positional: true, help: 'Stock name or code (e.g. 贵州茅台, 腾讯控股, AAPL)' },
64
+ { name: 'market', type: 'string', default: 'auto', help: 'Market: cn, hk, us, auto (default: auto searches cn → hk → us)' },
65
+ ],
66
+ columns: ['Symbol', 'Name', 'Price', 'Change', 'ChangePercent', 'Open', 'High', 'Low', 'Volume', 'MarketCap'],
67
+ func: async (_page, args) => {
68
+ const key = String(args.key);
69
+ const market = String(args.market);
70
+
71
+ const marketMap: Record<string, string[]> = {
72
+ cn: [MARKET_CN], hk: [MARKET_HK], us: [MARKET_US],
73
+ auto: [MARKET_CN, MARKET_HK, MARKET_US],
74
+ };
75
+ const targetMarkets = marketMap[market];
76
+ if (!targetMarkets) {
77
+ throw new CliError('INPUT_ERROR', `Invalid market: "${market}"`, 'Expected cn, hk, us, or auto');
78
+ }
79
+
80
+ // 1. Search symbol — only request the markets we care about
81
+ const suggestRaw = await fetchGBK(
82
+ `https://suggest3.sinajs.cn/suggest/type=${targetMarkets.join(',')}&key=${encodeURIComponent(key)}`
83
+ );
84
+ const entries = parseSuggest(suggestRaw, targetMarkets);
85
+ if (!entries.length) {
86
+ throw new CliError('NOT_FOUND', `No stock found for "${key}"`, 'Try a different name, code, or --market');
87
+ }
88
+
89
+ // Pick best match: score by name similarity, tiebreak by market priority
90
+ const needle = key.toLowerCase();
91
+ const score = (e: SuggestEntry): number => {
92
+ const n = e.name.toLowerCase();
93
+ if (n === needle) return 1;
94
+ if (n.includes(needle)) return needle.length / n.length;
95
+ return 0;
96
+ };
97
+ const best = entries.sort((a, b) => {
98
+ const d = score(b) - score(a);
99
+ return d !== 0 ? d : targetMarkets.indexOf(a.market) - targetMarkets.indexOf(b.market);
100
+ })[0];
101
+
102
+ // 2. Fetch quote
103
+ const sym = hqSymbol(best);
104
+ const hqRaw = await fetchGBK(`https://hq.sinajs.cn/list=${sym}`);
105
+ const f = parseHq(hqRaw, sym);
106
+
107
+ if (f.length < 2 || !f[0]) {
108
+ throw new CliError('NOT_FOUND', `No quote data for "${key}"`, 'Market may be closed or data unavailable');
109
+ }
110
+
111
+ if (best.market === MARKET_CN) {
112
+ const price = parseFloat(f[3]);
113
+ const prev = parseFloat(f[2]);
114
+ const chg = (price - prev).toFixed(2);
115
+ const chgPct = ((price - prev) / prev * 100).toFixed(2) + '%';
116
+ return [{ Symbol: sym.toUpperCase(), Name: f[0], Price: f[3], Change: chg, ChangePercent: chgPct, Open: f[1], High: f[4], Low: f[5], Volume: f[8], MarketCap: '' }];
117
+ }
118
+
119
+ if (best.market === MARKET_HK) {
120
+ // [2]=price [4]=high [5]=low [6]=open [7]=change [8]=change% [11]=volume
121
+ return [{ Symbol: best.symbol, Name: f[1], Price: f[2], Change: f[7], ChangePercent: f[8] + '%', Open: f[6], High: f[4], Low: f[5], Volume: f[11], MarketCap: '' }];
122
+ }
123
+
124
+ // MARKET_US: [1]=price [2]=change% [4]=change [6]=open [7]=today_low [8]=52wH [9]=52wL [10]=volume [12]=mktcap
125
+ return [{ Symbol: best.symbol.toUpperCase(), Name: f[0], Price: f[1], Change: f[4], ChangePercent: f[2] + '%', Open: f[6], High: f[8], Low: f[9], Volume: f[10], MarketCap: fmtMktCap(f[12]) }];
126
+ },
127
+ });