Skip to content

qoder 0.2.29: QwenWork (千问办公) — public cloud and enterprise flagship - #319

Open
LittlefishStudent wants to merge 2 commits into
magpie-community:mainfrom
LittlefishStudent:qwenwork-in-qoder
Open

LittlefishStudent wants to merge 2 commits into
magpie-community:mainfrom
LittlefishStudent:qwenwork-in-qoder

Conversation

@LittlefishStudent

Copy link
Copy Markdown

The single PR the ruling on #313/#314 asked for: QwenWork (千问办公) serves from the qoder package itself, as more provider ids beside qoder and qoder-cn, rebased on main (0.2.28's queue fix keeps its behaviour), at 0.2.29.

What

Provider id Service Sign-in
qwenwork the public cloud (gateway.qwenwork.cn) the desktop client's own device flow, with redirect_uri=qwenwork-cn://; usage reads its account-context
qwenwork-enterprise the enterprise flagship (企业旗舰版, VPC) asks for the enterprise's own domain (the host its admin console lives on), then the same device flow against that domain, with no redirect_uri — the flagship's /device/selectAccounts refuses one, as the desktop client's enterprise mode behaves

QWENWORK_ENT_DOMAIN presets the domain so the sign-in asks nothing.

Both reviews' items, addressed

From #313:

  • chat is signed for the site: cosyHeaders(CHAT_URL, cred, wire, siteOf(site, cred)), as are the model list and the queue paths — verified by a test that asserts the outgoing headers (ClientType 6, scene qwork, version 1.1.59, product qoder_work) and goes red when the site argument is dropped;
  • the hooks go red when removed: dropping the chat signing, the model scene, the 503 fallback or the enterpriseHost routing each fails tests — checked by hand for every hook;
  • package.json keeps its diff to the version, description and keywords lines;
  • the account field is enterpriseHost, not vpcDomain (that names a site's CN VPC suffix);
  • the README documents the protocol export and its not-a-stable-API caveat, and the QwenWork section.

From #314:

  • no cross-package import: one package, one version bump;
  • usage renews an expired account rather than marking it lapsed — a 401 from the account-context rotates the device token once, as qoder's own usage does; tested;
  • the public plugin offers no PAT and no check-in (pat: false, checkin: false): both are Qoder's, and Qoder's desktop device identity is never sent to QwenWork;
  • the public account is named email-first;
  • no bun.lock change, no separate package, no registry entry of its own: the qoder entry lists the two new provider ids.

Verified

  • end to end against a real deployment: sign-in by email OTP, flash and qwen3.8-max-preview chats, usage showing the Enterprise plan's credits;
  • HOME=$(mktemp -d) bun test: 2130 pass (Qoder and Qoder CN's 185 untouched); bun scripts/check.mjs: all ok.

Unofficial for QwenWork: not affiliated with Alibaba. @harwinvoid FYI — the qwenwork provider id your plugin served moves here.

@yetone yetone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @LittlefishStudent. This is the shape the ruling asked for, and the behaviour checks out. Only the tests stand between it and a merge.

What I ran at c228168

  • Whole repo, TMPDIR=$T HOME=$T/home bun test: 2127 pass, 0 fail. (HOME has to sit under tmpdir(), or the isolation guards in zcode and others throw.) packages/qoder alone: 195 pass. bun scripts/check.mjs: all ok.
  • scripts/try.sh qoder (magpie 0.1.1296, sandbox HOME): it lists qoder (17 models), qoder-cn (4), qwenwork (3) and qwenwork-enterprise (3).
  • A recording probe with fetch mocked. It records every request the plugin makes for a model list, a chat (the body decoded), and a usage read that gets 401 and then succeeds on a rotated token. Random fields are normalized.
    • Qoder and Qoder CN: base 4d73d22 and this PR give byte-identical recordings. Their behaviour doesn't change.
    • QwenWork: the list and the chat both go out with ClientType 6, scene qwork, version 1.1.59 and product qoder_work. The chat body has session_type: qoder_work, business.product: qoder_work and the QwenWork system prompt. Usage gets 401, refreshes with target: "c", reads again, and returns signIn: "kept". Only when the refresh is refused too does it return expired. There is no PAT method and no checkin. Machine ids are randomUUID(), and machineForCampaign runs only for qoder, so Qoder desktop's identity doesn't reach QwenWork.
  • qwenwork.cn is Alibaba's own service (whois, see #314), not a relay.

Blocking: hooks that no test catches. The ruling asked that "the signing and body hooks need tests that go red without them". I broke each hook by hand and ran packages/qoder:

Broken by hand Red
chat signed without site (index.mjs:2204) 1
model list signed without site (:619) 1
enterpriseHost routing (:256) 4
503 fallback (:625) 1
PAT / check-in off, normalizeUser 1 each
qoderBody(chat, m) without site (:2196) 0
systemPrompt (:897), sessionType (:916) 0
queue status signed without site (:1118) 0
the usage 401 rotation removed (:2045) 0
deviceRefreshTarget (:1687) 0
  1. The body hooks are the same gap as #313's item 3, still 0 red. _internal.decodeBody is already exported. Decode the recorded agent_chat_generation body in the chat test and assert session_type, business.product/version and the system text.
  2. The PR body says the usage 401 rotation is tested, but qwenwork.test.mjs:172-188 uses an expired access token, so fresh() renews it before account-context is ever asked. Please add the case the ruling named: a live access token, account-context answers 401 to it and 200 to the rotated token, and the test asserts signIn !== "expired" and that a refresh was made. Add a second case where the refresh is refused and the result is expired.
  3. Please also add tests for the queue status headers (a chat whose first answer is a queue refusal) and for the refresh body's target: "c".

Smaller

  1. qwenwork.test.mjs:118: the title says "to the enterprise's own host", but the test drives the public plugin and asserts no host. :128 (pub.loader?.()) is dead code, and qoderKeys at :100-105 is never used.
  2. index.mjs:1859-1860: "Overridable with QWENWORK_* env vars and defaults.json". Nothing reads either; QWENWORK_ENT_DOMAIN is the only variable. Please drop the line or make it true.
  3. index.mjs:328: a device-only account still stores email: "", even though the sign-in read it and accountId is now email-first. Unless QwenWork needs the field empty, keep the email, as Qoder does.

Once 1 to 3 are in, I'll merge.

@LittlefishStudent

Copy link
Copy Markdown
Author

All in at eeda75c — every hook that broke 0-red now has a test that goes red without it, and I broke each one again to check:

Broken by hand Red
qoderBody(chat, m) without site 1
systemPrompt 1
sessionType 1
queue status without site 1
the usage 401 rotation 1
deviceRefreshTarget 1
  1. The chat test decodes the recorded agent_chat_generation body (through _internal.decodeBody) and asserts session_type: "qoder_work", business.product: "qoder_work", business.version: "1.1.59" and the QwenWork system prompt — not Qoder's.
  2. The usage test now drives the named case: a live access token, account-context answering 401 to it and 200 to the rotated token, asserting signIn !== "expired" and that a refresh was made (counted); a second case where the refresh is refused gives signIn: "expired".
  3. The queue test makes a chat whose first answer is a code-10605 queue refusal and asserts the queue status request's Cosy headers for the site; the refresh test asserts the body's target: "c".

Smaller: the chat test's dead qoderKeys block and pub.loader?.() are gone (its title no longer claims a host it didn't check — the second, decoding test carries the body), the unread QWENWORK_* env vars line is dropped, and a device-only account keeps the email the sign-in read instead of blanking it.

Whole repo, your isolation style (TMPDIR and HOME under one temp dir): 2131 pass, 0 fail; check.mjs all ok; Qoder and Qoder CN's own tests untouched.

yetone added a commit that referenced this pull request Oct 22, 2026
…y, as its desktop and CLI do (yetone/magpie#2911, #304)

witt-bit (yetone/magpie#2911, 10-22): after Qoder CN's update moved its
daily 100 Credits into the desktop's 权益中心, magpie's Qoder CN check-in
stopped working; Andrew0767 (#304) sees the same with Qoder desktop
installed. The plugin sent Qoder CN's campaigns pages no device identity
at all ("Qoder CN does not need this local helper").

What Qoder CN's clients do, read from their own packages:
- Qoder CN desktop 0.4.9 (the .deb's out/main): its requests carry
  Cosy-ClientType 10 and, through createAuthorizedHeaders, the identity
  from <resources>/umid/runtime-info run with environment
  `regionCode === "global" ? 3 : 0`, i.e. 0 for Qoder CN, the uid on
  stdin (--account-stdin) on macOS and Windows. Campaigns are found
  through GET /sash/api/v1/me/notifications (hasClaimableCampaign,
  campaignPopupUrl). When the identity can't be read it sends the
  request without it (resolveMachineIdentity's catch returns null).
- qoderclicn (@qodercn-ai/qoderclicn 1.1.64, 1.1.70, 1.1.73):
  campaigns.list is GET /sash/api/v1/me/campaigns, needing the UMID from
  1.1.64 and requireMachineIdentity() from 1.1.70, UMID environment
  ONLINE = 0; its config dir is QODERCN_CONFIG_DIR, else QODERCN_CLI_HOME
  (or home) + QODERCN_CONFIG_DIR_NAME (or .qoder-cn), and it caches the
  helper in <config>/.bin/runtime-info-<platform>-<arch>-<hash>.
- Install places: Qoder CN.app/Contents/Resources/umid/runtime-info
  (seen inside Qoder-CN-Installer-mac-arm64.zip's payload, 0.4.9),
  /opt/Qoder-CN/resources/umid/runtime-info (the .deb), and on Windows
  umid/runtime-info.exe among Qoder-CN-win-x64-user.exe's owned files,
  with "Uninstall Qoder CN.exe" (electron-builder's
  %LOCALAPPDATA%\Programs\<productName>; the folder itself not seen).

The plugin now keeps a runtime per site (RUNTIMES): Qoder CN reads its own
helper (those paths, its CLI's folders, QODERCN_RUNTIME_INFO, never the
international Qoder's) with environment 0, and sends Cosy-MachineToken and
Cosy-MachineType on the list, the claim and the campaigns proxy. As the
desktop does, a Qoder CN request without a readable identity still goes
out, so an account that was claiming without Qoder CN installed (0.2.21's
live PAT claim) keeps doing so. When such a list offers no daily claim,
the check-in is ineligible for the day, saying to install Qoder CN desktop
or sign in to its CLI (no helper), or failed and asked again (a helper that
didn't answer), instead of "inactive". International Qoder is unchanged.

Run: in packages/qoder, TMPDIR=$H HOME=$H/home bun test: 195 pass, 0 fail.
On the old index.mjs the new tests are 9 red (checkin.test.mjs 5 lookup
tests and the env-0 proxy test, native-checkin.test.mjs the env-0 claim,
the ineligible and the failed cases). Repo root at this commit
(rebased on 26e3e5f): 2151 pass, 1 skip, 0 fail; bun scripts/check.mjs ok.

Not tried: a real Qoder CN account (none available) or a real Qoder CN
install, so whether Qoder CN now hides the daily claim from a request
without the identity is inferred from its clients, not seen on the wire;
the Windows install folder is electron-builder's default, not seen; the
desktop's own claim happens in a remote page (campaignPopupUrl) whose
calls weren't seen, so the list/claim pages stay those of 0.2.21;
Cosy-MachineCode, which both Qoder CN clients send, is not sent.
Version 0.2.29 is open PR #319's (QwenWork).
@yetone

yetone commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

main now holds qoder 0.2.30 (929a8b3, Qoder CN check-in device identity, touches packages/qoder/index.mjs, README.md, package.json and two tests). This PR needs a rebase and 0.2.31.

@LittlefishStudent

Copy link
Copy Markdown
Author

Rebased on main at 929a8b3 (qoder 0.2.30) and bumped to 0.2.31 — head is now 8f16bc1.

The 0.2.30 change (Qoder CN's check-in device identity) touches the campaigns path only; QwenWork's sites set checkin: false, so its behaviour is untouched, and the rebase's only conflict was the version line in package.json.

Verified after the rebase, your isolation style: whole repo 2264 pass / 0 fail, check.mjs all ok, packages/qoder 209 pass (QwenWork's 14 among them, all hooks still red when broken by hand as reported above).

@LittlefishStudent

Copy link
Copy Markdown
Author

Rebased again on main at 421117e (qoder 0.2.31) and bumped to 0.2.32 — head is now 935c5e9.

The 0.2.31 change (one task per prompt's tool loop, #330) reshaped qoderBody to (chat, m, named, task); the site hook moved to a fifth parameter, and its business line keeps the task's request_set_id/begin_at/stage semantics with the site's businessProduct/cosyVersion overriding the defaults. Conflicts were that call, its signature, and the version line.

Verified after the rebase: whole repo 2331 pass / 0 fail, check.mjs all ok, packages/qoder 215 pass; the hooks still go red when broken by hand (spot-checked qoderBody without its site under the new signature: 1 red).

Guislabert pushed a commit to Guislabert/plugins that referenced this pull request Oct 23, 2026
…y, as its desktop and CLI do (yetone/magpie#2911, magpie-community#304)

witt-bit (yetone/magpie#2911, 10-22): after Qoder CN's update moved its
daily 100 Credits into the desktop's 权益中心, magpie's Qoder CN check-in
stopped working; Andrew0767 (magpie-community#304) sees the same with Qoder desktop
installed. The plugin sent Qoder CN's campaigns pages no device identity
at all ("Qoder CN does not need this local helper").

What Qoder CN's clients do, read from their own packages:
- Qoder CN desktop 0.4.9 (the .deb's out/main): its requests carry
  Cosy-ClientType 10 and, through createAuthorizedHeaders, the identity
  from <resources>/umid/runtime-info run with environment
  `regionCode === "global" ? 3 : 0`, i.e. 0 for Qoder CN, the uid on
  stdin (--account-stdin) on macOS and Windows. Campaigns are found
  through GET /sash/api/v1/me/notifications (hasClaimableCampaign,
  campaignPopupUrl). When the identity can't be read it sends the
  request without it (resolveMachineIdentity's catch returns null).
- qoderclicn (@qodercn-ai/qoderclicn 1.1.64, 1.1.70, 1.1.73):
  campaigns.list is GET /sash/api/v1/me/campaigns, needing the UMID from
  1.1.64 and requireMachineIdentity() from 1.1.70, UMID environment
  ONLINE = 0; its config dir is QODERCN_CONFIG_DIR, else QODERCN_CLI_HOME
  (or home) + QODERCN_CONFIG_DIR_NAME (or .qoder-cn), and it caches the
  helper in <config>/.bin/runtime-info-<platform>-<arch>-<hash>.
- Install places: Qoder CN.app/Contents/Resources/umid/runtime-info
  (seen inside Qoder-CN-Installer-mac-arm64.zip's payload, 0.4.9),
  /opt/Qoder-CN/resources/umid/runtime-info (the .deb), and on Windows
  umid/runtime-info.exe among Qoder-CN-win-x64-user.exe's owned files,
  with "Uninstall Qoder CN.exe" (electron-builder's
  %LOCALAPPDATA%\Programs\<productName>; the folder itself not seen).

The plugin now keeps a runtime per site (RUNTIMES): Qoder CN reads its own
helper (those paths, its CLI's folders, QODERCN_RUNTIME_INFO, never the
international Qoder's) with environment 0, and sends Cosy-MachineToken and
Cosy-MachineType on the list, the claim and the campaigns proxy. As the
desktop does, a Qoder CN request without a readable identity still goes
out, so an account that was claiming without Qoder CN installed (0.2.21's
live PAT claim) keeps doing so. When such a list offers no daily claim,
the check-in is ineligible for the day, saying to install Qoder CN desktop
or sign in to its CLI (no helper), or failed and asked again (a helper that
didn't answer), instead of "inactive". International Qoder is unchanged.

Run: in packages/qoder, TMPDIR=$H HOME=$H/home bun test: 195 pass, 0 fail.
On the old index.mjs the new tests are 9 red (checkin.test.mjs 5 lookup
tests and the env-0 proxy test, native-checkin.test.mjs the env-0 claim,
the ineligible and the failed cases). Repo root at this commit
(rebased on 87df5a9): 2151 pass, 1 skip, 0 fail; bun scripts/check.mjs ok.

Not tried: a real Qoder CN account (none available) or a real Qoder CN
install, so whether Qoder CN now hides the daily claim from a request
without the identity is inferred from its clients, not seen on the wire;
the Windows install folder is electron-builder's default, not seen; the
desktop's own claim happens in a remote page (campaignPopupUrl) whose
calls weren't seen, so the list/claim pages stay those of 0.2.21;
Cosy-MachineCode, which both Qoder CN clients send, is not sent.
Version 0.2.29 is open PR magpie-community#319's (QwenWork).
Per the review of magpie-community#313/magpie-community#314, QwenWork serves from the qoder package
itself, as more provider ids beside qoder and qoder-cn, on site hooks
this commit adds to the protocol (rebased on main at qoder 0.2.33, so
the queue fix, Qoder CN's check-in, the per-task request_set_id and the
task's business name keep their behaviour):

- qwenwork: the public cloud (gateway.qwenwork.cn), the desktop client's
  own device flow with redirect_uri=qwenwork-cn://, its account-context
  for usage. pat: false and checkin: false: the PAT exchange and the
  campaigns endpoint are Qoder's, and Qoder's desktop device identity is
  never sent to QwenWork.
- qwenwork-enterprise: the enterprise flagship (企业旗舰版, VPC), which
  serves web/openapi/api on the enterprise's own single domain — asked at
  sign-in (refusing loopback/intranet suffixes, ports, paths and IPs;
  QWENWORK_ENT_DOMAIN presets it), routed to by the account's
  enterpriseHost for every call, its device flow sending no redirect_uri
  (the flagship's /device/selectAccounts refuses one). Accounts are named
  <email|uid> (<domain>); usage renews an expired account rather than
  lapsing it; a 503 catalog (the flagship deploys none) falls back to the
  shipped qwork list, which says in a comment its flags are copied from
  the public cloud, not checked against a flagship.
- chat, the model list, the queue paths and refresh all sign for the
  site: its client type, scene, version, product and RSA key; the chat
  body carries the site's session type, business product and system
  prompt. The account field is enterpriseHost, not vpcDomain (that names
  a site's CN VPC suffix); the public account is named email-first.
- registry: the qoder entry lists the two new provider ids.

Verified end to end against a real deployment (sign-in by email OTP,
flash and qwen3.8-max-preview chats, the Enterprise plan's credits);
unofficial, not affiliated with Alibaba.
Each hook the review broke by hand to find untested now has a test that
goes red without it — verified by breaking every one again:

- the chat test decodes the recorded agent_chat_generation body and
  asserts session_type qoder_work, business.product qoder_work, version
  1.1.59 and the QwenWork system prompt (red without qoderBody's site,
  systemPrompt or sessionType);
- a queue-refused chat (code 10605) makes the queue status request, whose
  headers are asserted for the site (red without queueStatus's site);
- usage with a live token that account-context refuses (401) rotates the
  device token once and stays signed in, counted; a refused refresh then
  expires it (red without the rotation);
- the device token refresh is asserted to be asked with target: "c"
  (red without deviceRefreshTarget);
- a device-only account keeps the email the sign-in read (it was blanked);
- the dead qoderKeys block and the QWENWORK_* env line the code doesn't
  read are gone.

All 223 of the package's tests pass with everything in place; the whole
repo gives 2461 pass / 0 fail; check.mjs all ok.
@LittlefishStudent

Copy link
Copy Markdown
Author

Rebased again on main (qoder 0.2.32 and 0.2.33 landed since) and bumped to 0.2.34 — head is now c543f1b.

0.2.33's change (a task's business name is the prompt's first ten characters) is kept: the business line now carries task.name alongside the site's businessProduct/cosyVersion overrides. The only other conflict was the version line.

Verified after the rebase: whole repo 2461 pass / 0 fail, check.mjs all ok (the cloudflare package needs the network, as on main), packages/qoder 223 pass; the hooks still go red when broken by hand.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants