mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-11-10 08:28:22 +00:00
refactor(dsh): collect environment management under src/environment
Move the Host modules out of src/pyenv and the tab out of src/client so the feature has one boundary: src/environment/types.ts browser-safe RPC contract src/environment/host/*.ts detection, install, service, workspace src/environment/client/* the Environment tab src/environment/host/gateway remeEnv RPC surface src/environment/host/manager the operations the gateway exposes The cross-boundary shapes (detection, reme-ai check, install job, .env summary, service state) now live in one browser-safe module instead of being pulled from Host modules that import node built-ins; Host-only shapes stay beside the code that owns them. src/index.ts registers the gateway and manager, src/client/index.tsx mounts the tab. The wire namespace, method names and service keys are unchanged.
This commit is contained in:
parent
11d2e90be7
commit
6261018436
19 changed files with 195 additions and 175 deletions
|
|
@ -4,11 +4,11 @@ import type { ConfigForm } from "@deepseek-ai/dsh-client-ui-settings/client";
|
|||
import type { ReMeSettings } from "../types.js";
|
||||
import { validEndpoint } from "../endpoint.js";
|
||||
import {
|
||||
ReMeEnvironmentTab,
|
||||
envEn,
|
||||
envZh,
|
||||
ReMeEnvironmentTab,
|
||||
type EnvTranslator,
|
||||
} from "./env-tab.js";
|
||||
} from "../environment/client/tab.js";
|
||||
import {
|
||||
ReMeStatusPage,
|
||||
statusEn,
|
||||
|
|
|
|||
|
|
@ -1,23 +1,22 @@
|
|||
import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
|
||||
import type { ConfigForm } from "@deepseek-ai/dsh-client-ui-settings/client";
|
||||
|
||||
import { safeError } from "../endpoint.js";
|
||||
import type { InstallStatus } from "../pyenv/install.js";
|
||||
import type { RemeCheck } from "../pyenv/reme.js";
|
||||
import type {
|
||||
ServiceLog,
|
||||
ServiceStartResult,
|
||||
ServiceState,
|
||||
} from "../pyenv/service.js";
|
||||
import { safeError } from "../../endpoint.js";
|
||||
import { ReMeClient } from "../../reme/client.js";
|
||||
import type { ReMeSettings } from "../../types.js";
|
||||
import type { StatusRpc } from "../../client/status-page.js";
|
||||
import type {
|
||||
EnvSummary,
|
||||
InstallStatus,
|
||||
LlmTestResult,
|
||||
PythonDetection,
|
||||
PythonEnv,
|
||||
PythonOrigin,
|
||||
} from "../pyenv/types.js";
|
||||
import type { EnvSummary, LlmTestResult } from "../pyenv/workspace.js";
|
||||
import { ReMeClient } from "../reme/client.js";
|
||||
import type { ReMeSettings } from "../types.js";
|
||||
import type { StatusRpc } from "./status-page.js";
|
||||
RemeCheck,
|
||||
ServiceLog,
|
||||
ServiceStartResult,
|
||||
ServiceState,
|
||||
} from "../types.js";
|
||||
|
||||
const MINICONDA_URL = "https://docs.conda.io/en/latest/miniconda.html";
|
||||
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import { join as posixJoin } from "node:path/posix";
|
||||
|
||||
import { safeError } from "../endpoint.js";
|
||||
import { safeError } from "../../endpoint.js";
|
||||
import type { RunCommand, RunResult } from "./runner.js";
|
||||
import { probeReme, remeVersionAtLeast } from "./reme.js";
|
||||
import type { PythonDetection, PythonEnv, PythonOrigin } from "./types.js";
|
||||
import type { PythonDetection, PythonEnv, PythonOrigin } from "../types.js";
|
||||
|
||||
/** ReMe requires Python 3.11 or newer. */
|
||||
export const MINIMUM_PYTHON = "3.11";
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
import type { Context } from "@deepseek-ai/cordis";
|
||||
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
||||
|
||||
import type { InstallStatus } from "./pyenv/install.js";
|
||||
import type { RemeCheck } from "./pyenv/reme.js";
|
||||
import type {
|
||||
EnvSummary,
|
||||
InstallStatus,
|
||||
LlmTestResult,
|
||||
PythonDetection,
|
||||
RemeCheck,
|
||||
ServiceLog,
|
||||
ServiceStartResult,
|
||||
ServiceState,
|
||||
} from "./pyenv/service.js";
|
||||
import type { PythonDetection } from "./pyenv/types.js";
|
||||
import type { EnvSummary, LlmTestResult } from "./pyenv/workspace.js";
|
||||
import type { ReMeEnvManager } from "./env-manager.js";
|
||||
} from "../types.js";
|
||||
import type { ReMeEnvManager } from "./manager.js";
|
||||
|
||||
declare module "@deepseek-ai/cordis" {
|
||||
interface Context {
|
||||
|
|
@ -2,30 +2,11 @@ import { spawn } from "node:child_process";
|
|||
import { closeSync, mkdirSync, openSync, readFileSync } from "node:fs";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
|
||||
export type InstallPhase = "running" | "succeeded" | "failed" | "cancelled";
|
||||
|
||||
export interface InstallRequest {
|
||||
/** Absolute interpreter that receives the packages. */
|
||||
pythonPath: string;
|
||||
/** Upgrade an existing installation to the newest satisfying version. */
|
||||
upgrade?: boolean;
|
||||
/** Optional package index URL (mirror); empty keeps the user's pip config. */
|
||||
indexUrl?: string;
|
||||
}
|
||||
|
||||
export interface InstallStatus {
|
||||
id: string;
|
||||
phase: InstallPhase;
|
||||
pythonPath: string;
|
||||
/** Effective command line, for display and diagnostics. */
|
||||
command: string;
|
||||
startedAt: string;
|
||||
finishedAt?: string;
|
||||
exitCode?: number | null;
|
||||
logPath: string;
|
||||
/** Tail of the install log; safe to poll. */
|
||||
logTail: string;
|
||||
}
|
||||
import type {
|
||||
InstallPhase,
|
||||
InstallRequest,
|
||||
InstallStatus,
|
||||
} from "../types.js";
|
||||
|
||||
export interface InstallChild {
|
||||
pid: number | undefined;
|
||||
|
|
@ -2,28 +2,22 @@ import { existsSync, readFileSync } from "node:fs";
|
|||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { detectPythonEnvs } from "./pyenv/detect.js";
|
||||
import { detectPythonEnvs } from "./detect.js";
|
||||
import {
|
||||
createInstallSpawner,
|
||||
InstallJobs,
|
||||
tailLog,
|
||||
type InstallRequest,
|
||||
type InstallStatus,
|
||||
} from "./pyenv/install.js";
|
||||
import { checkReme, remeVersionAtLeast, type RemeCheck } from "./pyenv/reme.js";
|
||||
import { createNodeRunner } from "./pyenv/runner.js";
|
||||
} from "./install.js";
|
||||
import { checkReme, remeVersionAtLeast } from "./reme.js";
|
||||
import { createNodeRunner } from "./runner.js";
|
||||
import {
|
||||
createServiceIO,
|
||||
readServiceState,
|
||||
serviceStatus,
|
||||
startService,
|
||||
stopService,
|
||||
type ServiceIO,
|
||||
type ServiceLog,
|
||||
type ServiceStartResult,
|
||||
type ServiceState,
|
||||
} from "./pyenv/service.js";
|
||||
import type { PythonDetection } from "./pyenv/types.js";
|
||||
} from "./service.js";
|
||||
import type { ServiceIO } from "./service.js";
|
||||
import {
|
||||
envSummary,
|
||||
parseEnvFile,
|
||||
|
|
@ -31,9 +25,18 @@ import {
|
|||
testLlm,
|
||||
validateEnvUpdate,
|
||||
writeEnvFile,
|
||||
type EnvSummary,
|
||||
type LlmTestResult,
|
||||
} from "./pyenv/workspace.js";
|
||||
} from "./workspace.js";
|
||||
import type {
|
||||
EnvSummary,
|
||||
InstallRequest,
|
||||
InstallStatus,
|
||||
LlmTestResult,
|
||||
PythonDetection,
|
||||
RemeCheck,
|
||||
ServiceLog,
|
||||
ServiceStartResult,
|
||||
ServiceState,
|
||||
} from "../types.js";
|
||||
|
||||
const CACHE_MS = 60_000;
|
||||
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { dirname, isAbsolute, join } from "node:path";
|
||||
|
||||
import type { RemeCheck, RemeProbe } from "../types.js";
|
||||
import type { RunCommand } from "./runner.js";
|
||||
|
||||
/** Minimum reme-ai version this DSH integration is tested against. */
|
||||
|
|
@ -14,24 +15,6 @@ export interface RemeIO {
|
|||
exists(path: string): boolean;
|
||||
}
|
||||
|
||||
export interface RemeProbe {
|
||||
installed: boolean;
|
||||
version: string;
|
||||
/** True when the version satisfies {@link MINIMUM_REME}. */
|
||||
versionOk: boolean;
|
||||
}
|
||||
|
||||
export interface RemeCheck extends RemeProbe {
|
||||
/** True when the installed version satisfies {@link MINIMUM_REME}. */
|
||||
versionOk: boolean;
|
||||
/** True when the AgentScope `[core]` extras import (agentscope) works. */
|
||||
hasCore: boolean;
|
||||
/** Absolute reme CLI path, or "" when only the python wrapper is available. */
|
||||
cliPath: string;
|
||||
/** Non-empty when the interpreter itself could not be probed. */
|
||||
error: string;
|
||||
}
|
||||
|
||||
/** Numeric dotted-version comparison; returns -1, 0 or 1. */
|
||||
export function compareVersions(a: string, b: string): number {
|
||||
const left = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
||||
|
|
@ -11,32 +11,7 @@ import {
|
|||
import { createConnection } from "node:net";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
export interface ServiceState {
|
||||
phase: "running" | "stopped" | "conflict";
|
||||
/** True when the running service was started by this plugin. */
|
||||
managed: boolean;
|
||||
endpoint: string;
|
||||
pid?: number;
|
||||
version?: string;
|
||||
startedAt?: string;
|
||||
logPath: string;
|
||||
}
|
||||
|
||||
export interface ServiceStartResult {
|
||||
started: boolean;
|
||||
alreadyRunning: boolean;
|
||||
pid?: number;
|
||||
version?: string;
|
||||
command: string;
|
||||
logPath: string;
|
||||
startedAt: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ServiceLog {
|
||||
logPath: string;
|
||||
lines: string;
|
||||
}
|
||||
import type { ServiceStartResult, ServiceState } from "../types.js";
|
||||
|
||||
export interface PersistedService {
|
||||
pid?: number;
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, isAbsolute, join, resolve } from "node:path";
|
||||
|
||||
import { safeError } from "../endpoint.js";
|
||||
import { safeError } from "../../endpoint.js";
|
||||
import type { EnvSummary, LlmTestResult } from "../types.js";
|
||||
import type { RunCommand } from "./runner.js";
|
||||
|
||||
/** Keys the Environment tab manages; split for the two form groups. */
|
||||
|
|
@ -20,31 +21,6 @@ export const EMBEDDING_KEYS = [
|
|||
const MANAGED_KEYS: readonly string[] = [...LLM_KEYS, ...EMBEDDING_KEYS];
|
||||
const SECRET_KEYS = new Set(["LLM_API_KEY", "EMBEDDING_API_KEY"]);
|
||||
|
||||
export interface EnvEntry {
|
||||
key: string;
|
||||
secret: boolean;
|
||||
/** True when the key has a non-empty value on disk. */
|
||||
set: boolean;
|
||||
/** Masked tail for secrets; the plain value for non-secret keys. */
|
||||
masked: string;
|
||||
/** Plain value for non-secret keys; empty for secrets. */
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface EnvSummary {
|
||||
workspaceDir: string;
|
||||
envPath: string;
|
||||
exists: boolean;
|
||||
entries: EnvEntry[];
|
||||
}
|
||||
|
||||
export interface LlmTestResult {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
latencyMs: number;
|
||||
error: string;
|
||||
}
|
||||
|
||||
/** Empty means "~/.reme"; "~" expands against home; relative paths are rejected. */
|
||||
export function resolveWorkspaceDir(raw: string, home: string): string {
|
||||
const trimmed = (raw ?? "").trim();
|
||||
136
integrations/dsh/src/environment/types.ts
Normal file
136
integrations/dsh/src/environment/types.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
/**
|
||||
* Environment contract shared by the DSH Host and the Environment tab.
|
||||
*
|
||||
* Everything here crosses the `remeEnv` RPC boundary, so it stays browser-safe:
|
||||
* this module must not import a node built-in. Host-only shapes (process I/O,
|
||||
* spawners, persisted state) live beside the code that owns them.
|
||||
*/
|
||||
|
||||
/** A local Python interpreter discovered on the DSH Host. */
|
||||
export type PythonOrigin =
|
||||
| { kind: "conda"; envName: string; root: string }
|
||||
| { kind: "python.org" }
|
||||
| { kind: "windows-store" }
|
||||
| { kind: "venv"; baseExe: string }
|
||||
| { kind: "system" }
|
||||
| { kind: "unknown" };
|
||||
|
||||
export interface PythonEnv {
|
||||
/** Verified interpreter executable (absolute, host-native separators). */
|
||||
exe: string;
|
||||
/** "major.minor.patch" reported by the interpreter; empty when unusable. */
|
||||
version: string;
|
||||
/** True when the interpreter answered and satisfies ReMe (Python >= 3.11). */
|
||||
ok: boolean;
|
||||
origin: PythonOrigin;
|
||||
/** Human-readable discovery channels, e.g. "conda env list; PATH". */
|
||||
source: string;
|
||||
/** reme-ai installation facts, probed for the interpreter recommendation. */
|
||||
reme?: { installed: boolean; version: string; versionOk?: boolean };
|
||||
}
|
||||
|
||||
export interface PythonDetection {
|
||||
platform: string;
|
||||
scannedAt: string;
|
||||
envs: PythonEnv[];
|
||||
/** Preferred interpreter for a local service, or "" when none is usable. */
|
||||
suggested: string;
|
||||
/** Conda installation root, or "" when conda was not found. */
|
||||
condaRoot: string;
|
||||
/** Non-fatal probe failures worth showing for transparency. */
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
/** Installed `reme-ai` facts for one interpreter. */
|
||||
export interface RemeProbe {
|
||||
installed: boolean;
|
||||
version: string;
|
||||
/** True when the version satisfies the minimum this integration targets. */
|
||||
versionOk: boolean;
|
||||
}
|
||||
|
||||
export interface RemeCheck extends RemeProbe {
|
||||
/** True when the AgentScope `[core]` extras import (agentscope) works. */
|
||||
hasCore: boolean;
|
||||
/** Absolute reme CLI path, or "" when only the python wrapper is available. */
|
||||
cliPath: string;
|
||||
/** Non-empty when the interpreter itself could not be probed. */
|
||||
error: string;
|
||||
}
|
||||
|
||||
export type InstallPhase = "running" | "succeeded" | "failed" | "cancelled";
|
||||
|
||||
export interface InstallRequest {
|
||||
/** Absolute interpreter that receives the packages. */
|
||||
pythonPath: string;
|
||||
/** Upgrade an existing installation to the newest satisfying version. */
|
||||
upgrade?: boolean;
|
||||
/** Optional package index URL (mirror); empty keeps the user's pip config. */
|
||||
indexUrl?: string;
|
||||
}
|
||||
|
||||
export interface InstallStatus {
|
||||
id: string;
|
||||
phase: InstallPhase;
|
||||
pythonPath: string;
|
||||
/** Effective command line, for display and diagnostics. */
|
||||
command: string;
|
||||
startedAt: string;
|
||||
finishedAt?: string;
|
||||
exitCode?: number | null;
|
||||
logPath: string;
|
||||
/** Tail of the install log; safe to poll. */
|
||||
logTail: string;
|
||||
}
|
||||
|
||||
export interface EnvEntry {
|
||||
key: string;
|
||||
secret: boolean;
|
||||
/** True when the key has a non-empty value on disk. */
|
||||
set: boolean;
|
||||
/** Masked tail for secrets; the plain value for non-secret keys. */
|
||||
masked: string;
|
||||
/** Plain value for non-secret keys; empty for secrets. */
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface EnvSummary {
|
||||
workspaceDir: string;
|
||||
envPath: string;
|
||||
exists: boolean;
|
||||
entries: EnvEntry[];
|
||||
}
|
||||
|
||||
export interface LlmTestResult {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
latencyMs: number;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface ServiceState {
|
||||
phase: "running" | "stopped" | "conflict";
|
||||
/** True when the running service was started by this plugin. */
|
||||
managed: boolean;
|
||||
endpoint: string;
|
||||
pid?: number;
|
||||
version?: string;
|
||||
startedAt?: string;
|
||||
logPath: string;
|
||||
}
|
||||
|
||||
export interface ServiceStartResult {
|
||||
started: boolean;
|
||||
alreadyRunning: boolean;
|
||||
pid?: number;
|
||||
version?: string;
|
||||
command: string;
|
||||
logPath: string;
|
||||
startedAt: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ServiceLog {
|
||||
logPath: string;
|
||||
lines: string;
|
||||
}
|
||||
|
|
@ -6,8 +6,8 @@ import type {} from "@deepseek-ai/dsh-session";
|
|||
import { ReMeClient } from "./reme/client.js";
|
||||
|
||||
import { resolveConfig } from "./config.js";
|
||||
import { ReMeEnvGateway } from "./env-gateway.js";
|
||||
import { ReMeEnvManager } from "./env-manager.js";
|
||||
import { ReMeEnvGateway } from "./environment/host/gateway.js";
|
||||
import { ReMeEnvManager } from "./environment/host/manager.js";
|
||||
import { hasGuidance, memoryGuidance, REME_PLUGIN_SOURCE } from "./guidance.js";
|
||||
import { ReMeRuntime } from "./runtime.js";
|
||||
import { ReMeStatusGateway } from "./status-gateway.js";
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
/** A local Python interpreter discovered on this machine. */
|
||||
export type PythonOrigin =
|
||||
| { kind: "conda"; envName: string; root: string }
|
||||
| { kind: "python.org" }
|
||||
| { kind: "windows-store" }
|
||||
| { kind: "venv"; baseExe: string }
|
||||
| { kind: "system" }
|
||||
| { kind: "unknown" };
|
||||
|
||||
export interface PythonEnv {
|
||||
/** Verified interpreter executable (absolute, host-native separators). */
|
||||
exe: string;
|
||||
/** "major.minor.patch" reported by the interpreter; empty when unusable. */
|
||||
version: string;
|
||||
/** True when the interpreter answered and satisfies ReMe (Python >= 3.11). */
|
||||
ok: boolean;
|
||||
origin: PythonOrigin;
|
||||
/** Human-readable discovery channels, e.g. "conda env list; PATH". */
|
||||
source: string;
|
||||
/** reme-ai installation facts, probed for the interpreter recommendation. */
|
||||
reme?: { installed: boolean; version: string; versionOk?: boolean };
|
||||
}
|
||||
|
||||
export interface PythonDetection {
|
||||
platform: string;
|
||||
scannedAt: string;
|
||||
envs: PythonEnv[];
|
||||
/** Preferred interpreter for a local service, or "" when none is usable. */
|
||||
suggested: string;
|
||||
/** Conda installation root, or "" when conda was not found. */
|
||||
condaRoot: string;
|
||||
/** Non-fatal probe failures worth showing for transparency. */
|
||||
errors: string[];
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ import {
|
|||
parseVerifyOutput,
|
||||
suggestInterpreter,
|
||||
versionAtLeast,
|
||||
} from "../dist/pyenv/detect.js";
|
||||
} from "../dist/environment/host/detect.js";
|
||||
|
||||
function fakeRun(rules) {
|
||||
return async (exe, args) => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
|||
import test from "node:test";
|
||||
import { Context } from "@deepseek-ai/cordis";
|
||||
|
||||
import { ReMeEnvGateway } from "../dist/env-gateway.js";
|
||||
import { ReMeEnvGateway } from "../dist/environment/host/gateway.js";
|
||||
|
||||
/** Manager stub: no subprocesses, just enough surface for the gateway. */
|
||||
function stubManager() {
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import {
|
|||
compareVersions,
|
||||
parseRemeVersion,
|
||||
remeVersionAtLeast,
|
||||
} from "../dist/pyenv/reme.js";
|
||||
import { InstallJobs, pipArgs, tailLog } from "../dist/pyenv/install.js";
|
||||
} from "../dist/environment/host/reme.js";
|
||||
import { InstallJobs, pipArgs, tailLog } from "../dist/environment/host/install.js";
|
||||
|
||||
const SNIPPETS = {
|
||||
version: "importlib.metadata",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
startService,
|
||||
stopService,
|
||||
writeServiceState,
|
||||
} from "../dist/pyenv/service.js";
|
||||
} from "../dist/environment/host/service.js";
|
||||
|
||||
function fakeIO(overrides = {}) {
|
||||
const calls = { killTree: [], spawn: [] };
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import test from "node:test";
|
|||
import React from "react";
|
||||
import { act, create } from "react-test-renderer";
|
||||
|
||||
import { ReMeEnvironmentTab, envZh } from "../dist/client/env-tab.js";
|
||||
import { ReMeEnvironmentTab, envZh } from "../dist/environment/client/tab.js";
|
||||
|
||||
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
resolveWorkspaceDir,
|
||||
testLlm,
|
||||
validateEnvUpdate,
|
||||
} from "../dist/pyenv/workspace.js";
|
||||
} from "../dist/environment/host/workspace.js";
|
||||
|
||||
test("resolves the workspace directory with ~/.reme defaults", () => {
|
||||
const home = process.platform === "win32" ? "C:\\Users\\u" : "/home/u";
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue