Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db6fd1b8e3 | ||
|
|
5176683937 | ||
|
|
6150ceb9cc | ||
|
|
6054f9921a | ||
|
|
0c59413ad8 | ||
|
|
625f877b43 |
No files matched your search
+16
-4
@@ -1,8 +1,20 @@
|
||||
# MemHub / One Memory
|
||||
# MemHub
|
||||
|
||||
MemHub 是一个本地优先的共享记忆产品:把用户授权的多个 AI 产品中的历史对话和新增内容
|
||||
汇总到同一份记忆,并让这些 AI 在独立授权后复用它。Hy Memory 是 MemHub 使用的 Python
|
||||
SDK 和记忆引擎。One Memory / `one-memory` 是历史模块、数据目录、服务和兼容命令名称。
|
||||
MemHub是一个本地优先的共享记忆产品。它把用户明确授权的AI对话和编码记录汇总到一份本地记忆中,再由用户分别授权给支持的AI工具调用。你可以继续使用熟悉的客户端,同时保留跨工具延续的上下文。Hy Memory是MemHub使用的Python SDK和记忆引擎。
|
||||
|
||||
# 💡❤️ MemHub 刚刚开源,欢迎一起优化共建! ❤️💡
|
||||
|
||||
**# 💭 MemHub处于preview阶段,当前公开版本可以下载和体验,部分平台制品、安装路径与宿主体验仍在持续优化验证。安装前请先查看Release里的实际制品和版本说明。**
|
||||
|
||||
> 🙋 如果你遇到问题/有建议:欢迎提交Issue,并附上MemHub版本、操作系统与芯片架构、复现步骤、预期结果、实际结果和经过脱敏的日志。请不要上传APIKey、真实对话、个人路径或其他敏感信息。
|
||||
|
||||
> 我们会公开跟进可复现的问题,持续发布修复,并根据真实反馈完善安装、同步和召回体验。
|
||||
|
||||
**# 感谢每一条反馈,期待你和我们一起打磨MemHub的体验 :)**
|
||||
|
||||
> 💗 MemHub has just been open-sourced and is under active development. The current public release is available for download andevaluation. Some platform packages, installation paths, and host integrations are still being completed and verified. Check the assets
|
||||
and release notes before installing.
|
||||
If you run into a problem, please open an Issue with the version, platform,reproduction steps, and sanitized logs, We wilreviewreports, publish fixes, and keep the support matrix up to date.
|
||||
|
||||
## 发行与平台合同
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import * as api from "./api";
|
||||
import { saveConnectionPreferences } from "./connection-preferences";
|
||||
import { resetConnectionReconcile } from "./connection-reconcile";
|
||||
import { queryClient, resetClient } from "./query-client";
|
||||
import { commitSedimentation } from "./sedimentation-state";
|
||||
|
||||
const {
|
||||
summary,
|
||||
@@ -701,28 +700,6 @@ it("shows a paused switch immediately even when task rows still say running", as
|
||||
expect(screen.queryByText("新的对话正在整理")).toBeNull();
|
||||
});
|
||||
|
||||
it("updates an already mounted home as soon as settings commits the switch receipt", async () => {
|
||||
render(
|
||||
<ApplicationsHome
|
||||
imports={imports}
|
||||
onManage={() => {}}
|
||||
onMemories={() => {}}
|
||||
onSettings={() => {}}
|
||||
/>,
|
||||
);
|
||||
expect(await screen.findAllByText("整理中")).toHaveLength(2);
|
||||
|
||||
act(() => {
|
||||
commitSedimentation({
|
||||
enabled: false,
|
||||
pending_retained: true,
|
||||
imports_unaffected: true,
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => expect(screen.getAllByText("已暂停")).toHaveLength(2));
|
||||
});
|
||||
|
||||
it("keeps the import-in-progress empty state stable without a progress link", async () => {
|
||||
summary.mockResolvedValue({
|
||||
memory_count: 0,
|
||||
|
||||
@@ -39,23 +39,3 @@ it("keeps a newer switch receipt when an older read lands later", async () => {
|
||||
expect(await olderRead).toEqual(paused);
|
||||
expect(queryClient.getQueryData(["sedimentation"])).toEqual(paused);
|
||||
});
|
||||
|
||||
it("keeps the newest read when concurrent reads finish out of order", async () => {
|
||||
let finishFirst: ((value: typeof enabled) => void) | undefined;
|
||||
getSedimentation
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<typeof enabled>((resolve) => {
|
||||
finishFirst = resolve;
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce(paused);
|
||||
|
||||
const first = refreshSedimentation();
|
||||
const second = refreshSedimentation();
|
||||
expect(await second).toEqual(paused);
|
||||
|
||||
finishFirst?.(enabled);
|
||||
expect(await first).toEqual(paused);
|
||||
expect(queryClient.getQueryData(["sedimentation"])).toEqual(paused);
|
||||
});
|
||||
@@ -11,12 +11,8 @@ queryClient.setQueryDefaults(SEDIMENTATION_KEY, {
|
||||
});
|
||||
|
||||
let revision = 0;
|
||||
let readsStarted = 0;
|
||||
let readsLanded = 0;
|
||||
onClientReset(() => {
|
||||
revision = 0;
|
||||
readsStarted = 0;
|
||||
readsLanded = 0;
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -24,14 +20,11 @@ onClientReset(() => {
|
||||
* newer PUT receipt committed by the settings page.
|
||||
*/
|
||||
export async function refreshSedimentation(): Promise<SedimentationState> {
|
||||
const order = ++readsStarted;
|
||||
const startedAtRevision = revision;
|
||||
const next = await getSedimentation();
|
||||
const current =
|
||||
queryClient.getQueryData<SedimentationState>(SEDIMENTATION_KEY);
|
||||
if (startedAtRevision !== revision && current) return current;
|
||||
if (order < readsLanded && current) return current;
|
||||
readsLanded = order;
|
||||
queryClient.setQueryData(SEDIMENTATION_KEY, next);
|
||||
return next;
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "one-memory",
|
||||
"version": "1.0.4",
|
||||
"release": {
|
||||
"build_number": 12,
|
||||
"build_number": 13,
|
||||
"macos_min_version": "13.0",
|
||||
"app_update": "manual",
|
||||
"runtime_manifest_schema": 1,
|
||||
|
||||
@@ -52,4 +52,21 @@ describe("domain errors", () => {
|
||||
expect(isDomainCode("")).toBe(false);
|
||||
expect(isDomainCode(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps only validated repo-relative stack locations", () => {
|
||||
const error = new DomainError("grant_required");
|
||||
error.stack = [
|
||||
"DomainError: private message",
|
||||
" at private (/Users/someone/private.ts:1:2)",
|
||||
" at safe (packages/host-bridge/src/domain-error.ts:88:10)",
|
||||
` at noisy (${`${"apps/".repeat(2_000)}bad`})`,
|
||||
].join("\n");
|
||||
|
||||
const diagnostic = diagnosticDomainError(error);
|
||||
|
||||
expect(diagnostic.safe_stack).toEqual([
|
||||
"packages/host-bridge/src/domain-error.ts:88:10",
|
||||
]);
|
||||
expect(JSON.stringify(diagnostic)).not.toContain("/Users/someone");
|
||||
});
|
||||
});
|
||||
@@ -66,6 +66,48 @@ export function domainCodeOf(error: unknown): DomainCode | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const SAFE_STACK_PREFIXES = ["apps/", "packages/", "scripts/"] as const;
|
||||
|
||||
function isSafeStackCharacter(character: string): boolean {
|
||||
const code = character.charCodeAt(0);
|
||||
return (
|
||||
(code >= 48 && code <= 57) ||
|
||||
(code >= 65 && code <= 90) ||
|
||||
(code >= 97 && code <= 122) ||
|
||||
"_./-:".includes(character)
|
||||
);
|
||||
}
|
||||
|
||||
function isAsciiDigits(value: string): boolean {
|
||||
if (!value.length) return false;
|
||||
for (const character of value) {
|
||||
const code = character.charCodeAt(0);
|
||||
if (code < 48 || code > 57) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Extract one repo-relative `path:line:column` in linear time. */
|
||||
function safeStackLocation(line: string): string | null {
|
||||
for (const prefix of SAFE_STACK_PREFIXES) {
|
||||
const start = line.indexOf(prefix);
|
||||
if (start < 0) continue;
|
||||
let end = start;
|
||||
while (end < line.length && isSafeStackCharacter(line[end] ?? "")) end += 1;
|
||||
const candidate = line.slice(start, end);
|
||||
const columnSeparator = candidate.lastIndexOf(":");
|
||||
const lineSeparator = candidate.lastIndexOf(":", columnSeparator - 1);
|
||||
if (
|
||||
lineSeparator > prefix.length &&
|
||||
columnSeparator > lineSeparator + 1 &&
|
||||
isAsciiDigits(candidate.slice(lineSeparator + 1, columnSeparator)) &&
|
||||
isAsciiDigits(candidate.slice(columnSeparator + 1))
|
||||
)
|
||||
return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The diagnostic fields for a failure, keeping the code when there is one.
|
||||
*
|
||||
* Only `error_type` and a repo-relative stack survive. `Error.message` is
|
||||
@@ -85,10 +127,8 @@ export function diagnosticDomainError(error: unknown): {
|
||||
const stack = (error.stack ?? "")
|
||||
.split("\n")
|
||||
.flatMap((line) => {
|
||||
const match = line.match(
|
||||
/((?:apps|packages|scripts)\/[A-Za-z0-9_./-]+:\d+:\d+)/,
|
||||
);
|
||||
return match?.[1] ? [match[1]] : [];
|
||||
const location = safeStackLocation(line);
|
||||
return location ? [location] : [];
|
||||
})
|
||||
.slice(0, 16);
|
||||
return {
|
||||
|
||||
Reference in new issue
Block a user