@chongqingliangyunzhijing/ssquant-cli 0.2.0 → 0.2.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 CHANGED
@@ -5,6 +5,8 @@
5
5
 
6
6
  ## 发给 AI
7
7
 
8
+ 开户完成后,账户列表显示柜台分配的实际交易账号,可直接用于 `account use` 和 `account delete`。开户中的请求可按操作号查询进度,已有内部编号仍可用于选择账户。
9
+
8
10
  > 请完整读取 https://www.pandaaiquant.com/ssquant/openapi/v1/agent-install-guide/cli ,按指引帮我安装 SSQUANT 期货仿真交易 CLI,使用我的 PandaAI 账号登录。请完成安装和环境检查,需要浏览器登录、验证码或授权时提醒我。登录后列出账户和剩余名额。如果我需要开户,问我初始资金(万元,1 至 10000),换算成人民币元,等我确认再创建。最后告诉我账户和就绪状态。本次仅做接入和开户。
9
11
 
10
12
  运行 `ssquant --help` 查看交易命令。查询、开户、交易计划和操作状态支持 `--json`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chongqingliangyunzhijing/ssquant-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "SSQUANT 期货仿真交易命令行工具",
5
5
  "type": "module",
6
6
  "bin": {
package/src/auth.js CHANGED
@@ -61,7 +61,7 @@ export async function loginBrowser({ scope = 'futures:read futures:trade', timeo
61
61
  const { verifier, challenge } = makePkce();
62
62
  const state = b64url(crypto.randomBytes(16));
63
63
 
64
- const { server, port, waitForCode } = await startCallbackServer(timeout);
64
+ const { server, port, waitForCode, finish } = await startCallbackServer(timeout, state);
65
65
  const redirectUri = `http://127.0.0.1:${port}/callback`;
66
66
 
67
67
  const url = apiUrl('/oauth/authorize') + '?' + new URLSearchParams({
@@ -93,42 +93,49 @@ export async function loginBrowser({ scope = 'futures:read futures:trade', timeo
93
93
  client_id: clientId(),
94
94
  },
95
95
  });
96
- return persist(data);
96
+ const saved = persist(data);
97
+ finish('success');
98
+ return saved;
99
+ } catch (error) {
100
+ finish('error');
101
+ throw error;
97
102
  } finally {
98
103
  server.close();
99
104
  }
100
105
  }
101
106
 
102
- function startCallbackServer(timeoutSec) {
107
+ export function startCallbackServer(timeoutSec, expectedState) {
103
108
  return new Promise((resolve, reject) => {
104
109
  let settle;
105
110
  const waitForCode = new Promise((res, rej) => {
106
111
  settle = { res, rej };
107
112
  });
108
113
 
114
+ let browserResponse;
115
+ const finish = (result) => {
116
+ if (!browserResponse || browserResponse.writableEnded) return;
117
+ browserResponse.writeHead(303, {
118
+ Location: LOGIN_HOME_URL + '?result=' + result,
119
+ 'Cache-Control': 'no-store',
120
+ 'Referrer-Policy': 'no-referrer',
121
+ }).end();
122
+ };
109
123
  const server = http.createServer((req, res) => {
110
124
  const u = new URL(req.url, 'http://127.0.0.1');
111
- if (!u.pathname.startsWith('/callback')) {
125
+ if (req.method !== 'GET' || u.pathname !== '/callback') {
112
126
  res.writeHead(404).end();
113
127
  return;
114
128
  }
115
- const err = u.searchParams.get('error');
116
- const html = (msg) =>
117
- `<!doctype html><meta charset="utf-8"><body style="font-family:system-ui;padding:40px">
118
- <h2>${msg}</h2><p>可以关闭本页面,回到命令行继续。</p></body>`;
119
- if (err) {
120
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
121
- .end(html('✗ 授权被拒绝'));
129
+ if (u.searchParams.get('state') !== expectedState || browserResponse ||
130
+ (!u.searchParams.get('code') && !u.searchParams.get('error'))) {
131
+ res.writeHead(400, { 'Cache-Control': 'no-store' }).end();
132
+ return;
133
+ }
134
+ browserResponse = res;
135
+ if (u.searchParams.get('error')) {
122
136
  settle.rej(new ApiError('access_denied', '你在浏览器中拒绝了授权'));
123
137
  return;
124
138
  }
125
- // 回调只负责交付一次性 code;交付后离开 localhost 页面,避免把 code
126
- // 留在地址栏或让用户看到一个简陋的本地确认页。
127
- res.writeHead(303, {
128
- Location: LOGIN_HOME_URL,
129
- 'Cache-Control': 'no-store',
130
- 'Referrer-Policy': 'no-referrer',
131
- }).end();
132
139
  settle.res({
133
140
  code: u.searchParams.get('code'),
134
141
  state: u.searchParams.get('state'),
@@ -144,7 +151,7 @@ function startCallbackServer(timeoutSec) {
144
151
 
145
152
  // 端口交给系统分配:固定端口会在多开时冲突
146
153
  server.listen(0, '127.0.0.1', () => {
147
- resolve({ server, port: server.address().port, waitForCode });
154
+ resolve({ server, port: server.address().port, waitForCode, finish });
148
155
  });
149
156
  server.on('error', reject);
150
157
  });
@@ -158,6 +165,7 @@ export function persist(d) {
158
165
  expiresAt: Date.now() + (d.expires_in || 2592000) * 1000,
159
166
  scope: d.scope,
160
167
  accountId: d.account_id,
168
+ tradingAccount: d.trading_account ?? null,
161
169
  market: d.market,
162
170
  clientId: clientId(),
163
171
  savedAt: new Date().toISOString(),
@@ -34,7 +34,7 @@ export async function create(opts) {
34
34
  export async function use(id) {
35
35
  const { data } = await request('/sim/accounts/' + encodeURIComponent(id) + '/select', { method:'POST' });
36
36
  persist(data);
37
- show({ accountId:id });
37
+ show({ accountId: data.trading_account ?? null, ...(!data.trading_account && { accountRef: data.account_id }) });
38
38
  }
39
39
  export async function remove(id, opts) {
40
40
  if (!opts.yes && !/^y(es)?$/i.test(await ask(`确认删除 ${id} 并释放名额?柜台已有委托仍可能成交。[y/N] `))) return;
@@ -5,7 +5,7 @@ import { emit, say } from '../output.js';
5
5
 
6
6
  export async function login() {
7
7
  const info = await loginBrowser();
8
- emit({ authenticated: true, accountId: info.accountId || null });
8
+ emit({ authenticated: true, accountId: info.tradingAccount || null });
9
9
  say('已登录 PandaAI。运行 ssquant account list 查看账户和名额。');
10
10
  }
11
11
  export function logout() {
package/src/http.js CHANGED
@@ -89,10 +89,23 @@ async function rawFetch(url, {
89
89
  return { res, json };
90
90
  }
91
91
 
92
+ // API 保留稳定的内部关联键,CLI 的账户号始终用柜台分配的交易账号。
93
+ function forDisplay(value, selectedAccount) {
94
+ if (Array.isArray(value)) return value.map(item => forDisplay(item, selectedAccount));
95
+ if (!value || typeof value !== 'object') return value;
96
+ const result = Object.fromEntries(Object.entries(value).map(([key, item]) => [key, forDisplay(item, selectedAccount)]));
97
+ if (typeof result.accountId === 'string' && result.accountId.startsWith('sq_')) {
98
+ const number = result.tradingAccount ?? selectedAccount ?? null;
99
+ if (!number) result.accountRef = result.accountId;
100
+ result.accountId = number;
101
+ }
102
+ return result;
103
+ }
104
+
92
105
  /** 服务端信封 → 结果或抛错 */
93
106
  function unwrap({ res, json }) {
94
107
  if (res.ok && json && json.code === '0') {
95
- return { data: json.data, meta: json.meta, requestId: json.request_id };
108
+ return { data: forDisplay(json.data, res.headers.get('X-SSQUANT-Trading-Account')), meta: json.meta, requestId: json.request_id };
96
109
  }
97
110
  const code = (json && json.code) || `http_${res.status}`;
98
111
  const message = (json && json.message) || `HTTP ${res.status}`;