Skip to content

Commit 8b9e330

Browse files
SWangHashSWangHash
authored andcommitted
!134 merge fix/ohos-agent-auto-install into main
feat(acp): add HarmonyOS Claude and Codex agents Created-by: guantw Commit-by: guantw Merged-by: SWangHash Description: ## Summary Add HarmonyOS-specific ACP Agent detection and installation support while preserving the existing behavior on Windows, Linux, macOS, and remote workspaces. HarmonyOS now supports: - Kimi Code - Qwen Code - CodeBuddy Code - Claude Code - Codex - DeepSeek Harness - OpenCode detection and guided acquisition The DeepSeek Harness ACP bridge is packaged into the HarmonyOS HAP and reused through its existing `bitfun-acp` profile. Fixes # ## Type and Areas Type: Feature / UI/UX Areas: HarmonyOS, ACP Agents, installer, web UI, Rust core, HAP packaging ## Motivation / Impact HarmonyOS users can detect, install, and add supported ACP Agents directly from BitFun. Agent definitions now select the appropriate implementation for the current platform. Existing Windows, Linux, macOS, and remote-workspace behavior remains unchanged. OpenCode does not modify the user's third-party HarmonyBrew trust configuration. If an existing installation is detected, it can be added directly; otherwise, BitFun provides the corresponding installation source. ## Verification - Passed focused ACP Agent frontend tests. - Passed DeepSeek Harness ACP bridge tests. - Passed TypeScript type checking and Rust formatting checks. - Successfully built the production HarmonyOS HAP. - Verified the packaged HAP contains the required DeepSeek Harness ACP resources. - Installed and tested the HAP on a physical HarmonyOS device. - Verified Agent detection, installation, configuration, and ACP startup flows on the device. ## Reviewer Notes HarmonyOS uses platform-specific provisioning definitions. Other desktop platforms and remote workspaces continue using the existing upstream Agent definitions. The implementation does not automatically modify or replace the user's HarmonyBrew tap trust configuration. ## Checklist - [x] This PR is focused and does not include secrets, temporary prompts, generated scratch files, or unrelated artifacts. - [x] Relevant verification is recorded above, or skipped checks are explained. - [x] User-facing strings, docs, and locales are updated where applicable. See merge request: OpenHarmonyPCDeveloper/BitFun!134
2 parents c458816 + dfc54b3 commit 8b9e330

13 files changed

Lines changed: 953 additions & 265 deletions

File tree

‎scripts/prepare-dsh-profile.mjs‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,14 @@
3131

3232
import { spawnSync } from 'node:child_process';
3333
import {
34+
cpSync,
3435
existsSync,
3536
lstatSync,
3637
mkdirSync,
3738
readdirSync,
3839
readFileSync,
40+
renameSync,
41+
rmSync,
3942
statSync,
4043
writeFileSync,
4144
} from 'node:fs';
@@ -45,7 +48,22 @@ import { fileURLToPath } from 'node:url';
4548
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
4649
const PACKAGE_DIR = path.join(ROOT_DIR, 'packages', 'dsh-acp');
4750
const OUT_DIR = path.join(PACKAGE_DIR, 'dist-profile');
51+
const OHOS_OUT_DIR = path.join(
52+
ROOT_DIR,
53+
'src',
54+
'apps',
55+
'ohos',
56+
'entry',
57+
'src',
58+
'main',
59+
'resources',
60+
'resfile',
61+
'dsh-profile',
62+
);
4863
const STAMP_FILENAME = '.bitfun-bridge.json';
64+
const OHOS_STAMP_FILENAME = 'bitfun-bridge.json';
65+
const NODE_MODULES_DIR = 'node_modules';
66+
const OHOS_NODE_MODULES_DIR = 'vendor-node-modules';
4967

5068
/** One installed harness package is enough to tell a populated tree from a bare one. */
5169
const DEPS_PROBE = path.join(PACKAGE_DIR, 'node_modules', '@deepseek-ai', 'dsh-app-boot');
@@ -185,6 +203,35 @@ export function getDshProfileRebuildPlan({
185203
};
186204
}
187205

206+
/**
207+
* Stage the built profile into HarmonyOS resfile-compatible names.
208+
*
209+
* The HarmonyOS resource bundle uses aliases for the profile's leading-dot
210+
* marker and `node_modules` directory. Keep the canonical desktop build
211+
* unchanged and normalize the names back when the Rust client materializes the
212+
* profile into the user's DSH home.
213+
*
214+
* @param {{ sourceDir?: string, destinationDir?: string }} [options]
215+
*/
216+
export function syncOhosDshProfile({
217+
sourceDir = OUT_DIR,
218+
destinationDir = OHOS_OUT_DIR,
219+
} = {}) {
220+
rmSync(destinationDir, { recursive: true, force: true });
221+
mkdirSync(path.dirname(destinationDir), { recursive: true });
222+
cpSync(sourceDir, destinationDir, { recursive: true, dereference: true });
223+
224+
const stamp = path.join(destinationDir, STAMP_FILENAME);
225+
if (existsSync(stamp)) {
226+
renameSync(stamp, path.join(destinationDir, OHOS_STAMP_FILENAME));
227+
}
228+
229+
const nodeModules = path.join(destinationDir, NODE_MODULES_DIR);
230+
if (existsSync(nodeModules)) {
231+
renameSync(nodeModules, path.join(destinationDir, OHOS_NODE_MODULES_DIR));
232+
}
233+
}
234+
188235
/**
189236
* Run a command in the bridge package directory.
190237
* @param {string} command - the executable to run.
@@ -217,8 +264,10 @@ function fail(message, status) {
217264
function main() {
218265
if (process.env.BITFUN_SKIP_DSH_PROFILE === '1') {
219266
process.stdout.write('[dsh-profile] skipped (this build ships no DeepSeek bridge)\n');
267+
rmSync(OUT_DIR, { recursive: true, force: true });
220268
mkdirSync(OUT_DIR, { recursive: true });
221269
writeFileSync(path.join(OUT_DIR, 'NOT-BUILT.md'), PLACEHOLDER);
270+
syncOhosDshProfile();
222271
return;
223272
}
224273

@@ -227,6 +276,7 @@ function main() {
227276
const plan = getDshProfileRebuildPlan({ force });
228277
if (!plan.shouldBuild) {
229278
process.stdout.write(`[dsh-profile] ${plan.reason}\n`);
279+
syncOhosDshProfile();
230280
return;
231281
}
232282
process.stdout.write(`[dsh-profile] ${plan.reason}\n`);
@@ -242,6 +292,8 @@ function main() {
242292

243293
const packaged = run('node', ['scripts/build-profile.mjs']);
244294
if (packaged !== 0) fail('profile packaging failed', packaged);
295+
296+
syncOhosDshProfile();
245297
}
246298

247299
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {

‎scripts/prepare-dsh-profile.test.mjs‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import assert from 'node:assert/strict';
22
import {
3+
existsSync,
34
mkdirSync,
45
mkdtempSync,
56
readFileSync,
@@ -12,7 +13,10 @@ import path from 'node:path';
1213
import { test } from 'node:test';
1314
import { fileURLToPath } from 'node:url';
1415

15-
import { getDshProfileRebuildPlan } from './prepare-dsh-profile.mjs';
16+
import {
17+
getDshProfileRebuildPlan,
18+
syncOhosDshProfile,
19+
} from './prepare-dsh-profile.mjs';
1620

1721
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
1822

@@ -149,6 +153,34 @@ test('skips when a complete stamp is newer than every input', () => {
149153
}
150154
});
151155

156+
test('stages a clean HarmonyOS profile with resfile-compatible names', () => {
157+
const root = mkdtempSync(path.join(tmpdir(), 'bitfun-dsh-ohos-profile-'));
158+
const sourceDir = path.join(root, 'dist-profile');
159+
const destinationDir = path.join(root, 'resfile', 'dsh-profile');
160+
try {
161+
mkdirSync(path.join(sourceDir, 'node_modules', '@example', 'bridge'), { recursive: true });
162+
writeFileSync(path.join(sourceDir, '.bitfun-bridge.json'), '{"profile":"bitfun-acp"}\n');
163+
writeFileSync(path.join(sourceDir, 'node_modules', '@example', 'bridge', 'index.js'), '');
164+
mkdirSync(destinationDir, { recursive: true });
165+
writeFileSync(path.join(destinationDir, 'stale.js'), 'stale');
166+
167+
syncOhosDshProfile({ sourceDir, destinationDir });
168+
169+
assert.equal(existsSync(path.join(destinationDir, 'stale.js')), false);
170+
assert.equal(existsSync(path.join(destinationDir, '.bitfun-bridge.json')), false);
171+
assert.equal(existsSync(path.join(destinationDir, 'node_modules')), false);
172+
assert.equal(existsSync(path.join(destinationDir, 'bitfun-bridge.json')), true);
173+
assert.equal(
174+
existsSync(
175+
path.join(destinationDir, 'vendor-node-modules', '@example', 'bridge', 'index.js'),
176+
),
177+
true,
178+
);
179+
} finally {
180+
rmSync(root, { recursive: true, force: true });
181+
}
182+
});
183+
152184
test('desktop:dev does not compile the DeepSeek profile', () => {
153185
const devScript = readFileSync(path.join(repoRoot, 'scripts', 'dev.cjs'), 'utf8');
154186
assert.doesNotMatch(devScript, /runCommandPrefixed\('dsh-profile'/);

0 commit comments

Comments
 (0)