feat(android-sdk): 添加完整的IM客户端SDK实现

- 实现了Android SDK的完整IM功能接口,包括消息、群组、好友、会话等核心功能
- 添加了消息收发、历史记录、撤回编辑等完整的消息操作能力
- 实现了群组管理功能,包括创建、成员管理、权限设置等操作
- 添加了好友关系链管理,支持添加、删除、分组等操作
- 实现了会话管理功能,包括置顶、免打扰、已读状态等
- 添加了黑名单、资料管理、搜索等辅助功能
- 补齐了批量操作接口,提升客户端操作效率
- 实现了WebSocket连接管理和事件监听机制
- 添加了离线消息同步和状态管理功能
这个提交包含在:
XuqmGroup 2026-05-02 22:57:55 +08:00
父节点 9406f21145
当前提交 6cd938cfbc
共有 53 个文件被更改,包括 37631 次插入70 次删除

查看文件

@ -0,0 +1,275 @@
import {
useMediaQuery
} from "./chunk-EBBFFI5H.js";
import {
computed,
ref,
shallowRef,
watch
} from "./chunk-XZXUNI6J.js";
// ../node_modules/vitepress/dist/client/theme-default/index.js
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/fonts.css";
// ../node_modules/vitepress/dist/client/theme-default/without-fonts.js
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/vars.css";
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/base.css";
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/icons.css";
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/utils.css";
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/components/custom-block.css";
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code.css";
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code-group.css";
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/components/vp-doc.css";
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/components/vp-sponsor.css";
import VPBadge from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue";
import Layout from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/Layout.vue";
import { default as default2 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue";
import { default as default3 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPButton.vue";
import { default as default4 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPDocAsideSponsors.vue";
import { default as default5 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPFeatures.vue";
import { default as default6 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPHomeContent.vue";
import { default as default7 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPHomeFeatures.vue";
import { default as default8 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPHomeHero.vue";
import { default as default9 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPHomeSponsors.vue";
import { default as default10 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPImage.vue";
import { default as default11 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPLink.vue";
import { default as default12 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPNavBarSearch.vue";
import { default as default13 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPSocialLink.vue";
import { default as default14 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPSocialLinks.vue";
import { default as default15 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPSponsors.vue";
import { default as default16 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPTeamMembers.vue";
import { default as default17 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPTeamPage.vue";
import { default as default18 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageSection.vue";
import { default as default19 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageTitle.vue";
// ../node_modules/vitepress/dist/client/theme-default/composables/local-nav.js
import { onContentUpdated } from "vitepress";
// ../node_modules/vitepress/dist/client/theme-default/composables/outline.js
import { getScrollOffset } from "vitepress";
// ../node_modules/vitepress/dist/client/theme-default/support/utils.js
import { withBase } from "vitepress";
// ../node_modules/vitepress/dist/client/theme-default/composables/data.js
import { useData as useData$ } from "vitepress";
var useData = useData$;
// ../node_modules/vitepress/dist/client/theme-default/support/utils.js
function ensureStartingSlash(path) {
return path.startsWith("/") ? path : `/${path}`;
}
// ../node_modules/vitepress/dist/client/theme-default/support/sidebar.js
function getSidebar(_sidebar, path) {
if (Array.isArray(_sidebar))
return addBase(_sidebar);
if (_sidebar == null)
return [];
path = ensureStartingSlash(path);
const dir = Object.keys(_sidebar).sort((a, b) => {
return b.split("/").length - a.split("/").length;
}).find((dir2) => {
return path.startsWith(ensureStartingSlash(dir2));
});
const sidebar = dir ? _sidebar[dir] : [];
return Array.isArray(sidebar) ? addBase(sidebar) : addBase(sidebar.items, sidebar.base);
}
function getSidebarGroups(sidebar) {
const groups = [];
let lastGroupIndex = 0;
for (const index in sidebar) {
const item = sidebar[index];
if (item.items) {
lastGroupIndex = groups.push(item);
continue;
}
if (!groups[lastGroupIndex]) {
groups.push({ items: [] });
}
groups[lastGroupIndex].items.push(item);
}
return groups;
}
function addBase(items, _base) {
return [...items].map((_item) => {
const item = { ..._item };
const base = item.base || _base;
if (base && item.link)
item.link = base + item.link;
if (item.items)
item.items = addBase(item.items, base);
return item;
});
}
// ../node_modules/vitepress/dist/client/theme-default/composables/sidebar.js
function useSidebar() {
const { frontmatter, page, theme: theme2 } = useData();
const is960 = useMediaQuery("(min-width: 960px)");
const isOpen = ref(false);
const _sidebar = computed(() => {
const sidebarConfig = theme2.value.sidebar;
const relativePath = page.value.relativePath;
return sidebarConfig ? getSidebar(sidebarConfig, relativePath) : [];
});
const sidebar = ref(_sidebar.value);
watch(_sidebar, (next, prev) => {
if (JSON.stringify(next) !== JSON.stringify(prev))
sidebar.value = _sidebar.value;
});
const hasSidebar = computed(() => {
return frontmatter.value.sidebar !== false && sidebar.value.length > 0 && frontmatter.value.layout !== "home";
});
const leftAside = computed(() => {
if (hasAside)
return frontmatter.value.aside == null ? theme2.value.aside === "left" : frontmatter.value.aside === "left";
return false;
});
const hasAside = computed(() => {
if (frontmatter.value.layout === "home")
return false;
if (frontmatter.value.aside != null)
return !!frontmatter.value.aside;
return theme2.value.aside !== false;
});
const isSidebarEnabled = computed(() => hasSidebar.value && is960.value);
const sidebarGroups = computed(() => {
return hasSidebar.value ? getSidebarGroups(sidebar.value) : [];
});
function open() {
isOpen.value = true;
}
function close() {
isOpen.value = false;
}
function toggle() {
isOpen.value ? close() : open();
}
return {
isOpen,
sidebar,
sidebarGroups,
hasSidebar,
hasAside,
leftAside,
isSidebarEnabled,
open,
close,
toggle
};
}
// ../node_modules/vitepress/dist/client/theme-default/composables/outline.js
var ignoreRE = /\b(?:VPBadge|header-anchor|footnote-ref|ignore-header)\b/;
var resolvedHeaders = [];
function getHeaders(range) {
const headers = [
...document.querySelectorAll(".VPDoc :where(h1,h2,h3,h4,h5,h6)")
].filter((el) => el.id && el.hasChildNodes()).map((el) => {
const level = Number(el.tagName[1]);
return {
element: el,
title: serializeHeader(el),
link: "#" + el.id,
level
};
});
return resolveHeaders(headers, range);
}
function serializeHeader(h) {
let ret = "";
for (const node of h.childNodes) {
if (node.nodeType === 1) {
if (ignoreRE.test(node.className))
continue;
ret += node.textContent;
} else if (node.nodeType === 3) {
ret += node.textContent;
}
}
return ret.trim();
}
function resolveHeaders(headers, range) {
if (range === false) {
return [];
}
const levelsRange = (typeof range === "object" && !Array.isArray(range) ? range.level : range) || 2;
const [high, low] = typeof levelsRange === "number" ? [levelsRange, levelsRange] : levelsRange === "deep" ? [2, 6] : levelsRange;
return buildTree(headers, high, low);
}
function buildTree(data, min, max) {
resolvedHeaders.length = 0;
const result = [];
const stack = [];
data.forEach((item) => {
const node = { ...item, children: [] };
let parent = stack[stack.length - 1];
while (parent && parent.level >= node.level) {
stack.pop();
parent = stack[stack.length - 1];
}
if (node.element.classList.contains("ignore-header") || parent && "shouldIgnore" in parent) {
stack.push({ level: node.level, shouldIgnore: true });
return;
}
if (node.level > max || node.level < min)
return;
resolvedHeaders.push({ element: node.element, link: node.link });
if (parent)
parent.children.push(node);
else
result.push(node);
stack.push(node);
});
return result;
}
// ../node_modules/vitepress/dist/client/theme-default/composables/local-nav.js
function useLocalNav() {
const { theme: theme2, frontmatter } = useData();
const headers = shallowRef([]);
const hasLocalNav = computed(() => {
return headers.value.length > 0;
});
onContentUpdated(() => {
headers.value = getHeaders(frontmatter.value.outline ?? theme2.value.outline);
});
return {
headers,
hasLocalNav
};
}
// ../node_modules/vitepress/dist/client/theme-default/without-fonts.js
var theme = {
Layout,
enhanceApp: ({ app }) => {
app.component("Badge", VPBadge);
}
};
var without_fonts_default = theme;
export {
default2 as VPBadge,
default3 as VPButton,
default4 as VPDocAsideSponsors,
default5 as VPFeatures,
default6 as VPHomeContent,
default7 as VPHomeFeatures,
default8 as VPHomeHero,
default9 as VPHomeSponsors,
default10 as VPImage,
default11 as VPLink,
default12 as VPNavBarSearch,
default13 as VPSocialLink,
default14 as VPSocialLinks,
default15 as VPSponsors,
default16 as VPTeamMembers,
default17 as VPTeamPage,
default18 as VPTeamPageSection,
default19 as VPTeamPageTitle,
without_fonts_default as default,
useLocalNav,
useSidebar
};
//# sourceMappingURL=@theme_index.js.map

文件差异因一行或多行过长而隐藏

查看文件

@ -0,0 +1,58 @@
{
"hash": "2a36dde7",
"configHash": "739fef51",
"lockfileHash": "2d1a3afd",
"browserHash": "7d1e9e84",
"optimized": {
"vue": {
"src": "../../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
"fileHash": "0e8b32ca",
"needsInterop": false
},
"vitepress > @vue/devtools-api": {
"src": "../../../../../node_modules/@vue/devtools-api/dist/index.js",
"file": "vitepress___@vue_devtools-api.js",
"fileHash": "732e7221",
"needsInterop": false
},
"vitepress > @vueuse/core": {
"src": "../../../../../node_modules/@vueuse/core/index.mjs",
"file": "vitepress___@vueuse_core.js",
"fileHash": "fd9749e3",
"needsInterop": false
},
"vitepress > @vueuse/integrations/useFocusTrap": {
"src": "../../../../../node_modules/@vueuse/integrations/useFocusTrap.mjs",
"file": "vitepress___@vueuse_integrations_useFocusTrap.js",
"fileHash": "5e56c8b9",
"needsInterop": false
},
"vitepress > mark.js/src/vanilla.js": {
"src": "../../../../../node_modules/mark.js/src/vanilla.js",
"file": "vitepress___mark__js_src_vanilla__js.js",
"fileHash": "84c915aa",
"needsInterop": false
},
"vitepress > minisearch": {
"src": "../../../../../node_modules/minisearch/dist/es/index.js",
"file": "vitepress___minisearch.js",
"fileHash": "e5b43ef7",
"needsInterop": false
},
"@theme/index": {
"src": "../../../../../node_modules/vitepress/dist/client/theme-default/index.js",
"file": "@theme_index.js",
"fileHash": "b709525e",
"needsInterop": false
}
},
"chunks": {
"chunk-EBBFFI5H": {
"file": "chunk-EBBFFI5H.js"
},
"chunk-XZXUNI6J": {
"file": "chunk-XZXUNI6J.js"
}
}
}

文件差异内容过多而无法显示 加载差异

文件差异因一行或多行过长而隐藏

文件差异内容过多而无法显示 加载差异

文件差异因一行或多行过长而隐藏

查看文件

@ -0,0 +1,3 @@
{
"type": "module"
}

文件差异内容过多而无法显示 加载差异

文件差异因一行或多行过长而隐藏

查看文件

@ -0,0 +1,583 @@
import {
DefaultMagicKeysAliasMap,
StorageSerializers,
TransitionPresets,
assert,
breakpointsAntDesign,
breakpointsBootstrapV5,
breakpointsElement,
breakpointsMasterCss,
breakpointsPrimeFlex,
breakpointsQuasar,
breakpointsSematic,
breakpointsTailwind,
breakpointsVuetify,
breakpointsVuetifyV2,
breakpointsVuetifyV3,
bypassFilter,
camelize,
clamp,
cloneFnJSON,
computedAsync,
computedEager,
computedInject,
computedWithControl,
containsProp,
controlledRef,
createEventHook,
createFetch,
createFilterWrapper,
createGlobalState,
createInjectionState,
createRef,
createReusableTemplate,
createSharedComposable,
createSingletonPromise,
createTemplatePromise,
createUnrefFn,
customStorageEventName,
debounceFilter,
defaultDocument,
defaultLocation,
defaultNavigator,
defaultWindow,
executeTransition,
extendRef,
formatDate,
formatTimeAgo,
get,
getLifeCycleTarget,
getSSRHandler,
hasOwn,
hyphenate,
identity,
increaseWithUnit,
injectLocal,
invoke,
isClient,
isDef,
isDefined,
isIOS,
isObject,
isWorker,
makeDestructurable,
mapGamepadToXbox360Controller,
noop,
normalizeDate,
notNullish,
now,
objectEntries,
objectOmit,
objectPick,
onClickOutside,
onElementRemoval,
onKeyDown,
onKeyPressed,
onKeyStroke,
onKeyUp,
onLongPress,
onStartTyping,
pausableFilter,
promiseTimeout,
provideLocal,
provideSSRWidth,
pxValue,
rand,
reactify,
reactifyObject,
reactiveComputed,
reactiveOmit,
reactivePick,
refAutoReset,
refDebounced,
refDefault,
refThrottled,
refWithControl,
resolveRef,
resolveUnref,
set,
setSSRHandler,
syncRef,
syncRefs,
templateRef,
throttleFilter,
timestamp,
toArray,
toReactive,
toRef,
toRefs,
toValue,
tryOnBeforeMount,
tryOnBeforeUnmount,
tryOnMounted,
tryOnScopeDispose,
tryOnUnmounted,
unrefElement,
until,
useActiveElement,
useAnimate,
useArrayDifference,
useArrayEvery,
useArrayFilter,
useArrayFind,
useArrayFindIndex,
useArrayFindLast,
useArrayIncludes,
useArrayJoin,
useArrayMap,
useArrayReduce,
useArraySome,
useArrayUnique,
useAsyncQueue,
useAsyncState,
useBase64,
useBattery,
useBluetooth,
useBreakpoints,
useBroadcastChannel,
useBrowserLocation,
useCached,
useClipboard,
useClipboardItems,
useCloned,
useColorMode,
useConfirmDialog,
useCountdown,
useCounter,
useCssVar,
useCurrentElement,
useCycleList,
useDark,
useDateFormat,
useDebounceFn,
useDebouncedRefHistory,
useDeviceMotion,
useDeviceOrientation,
useDevicePixelRatio,
useDevicesList,
useDisplayMedia,
useDocumentVisibility,
useDraggable,
useDropZone,
useElementBounding,
useElementByPoint,
useElementHover,
useElementSize,
useElementVisibility,
useEventBus,
useEventListener,
useEventSource,
useEyeDropper,
useFavicon,
useFetch,
useFileDialog,
useFileSystemAccess,
useFocus,
useFocusWithin,
useFps,
useFullscreen,
useGamepad,
useGeolocation,
useIdle,
useImage,
useInfiniteScroll,
useIntersectionObserver,
useInterval,
useIntervalFn,
useKeyModifier,
useLastChanged,
useLocalStorage,
useMagicKeys,
useManualRefHistory,
useMediaControls,
useMediaQuery,
useMemoize,
useMemory,
useMounted,
useMouse,
useMouseInElement,
useMousePressed,
useMutationObserver,
useNavigatorLanguage,
useNetwork,
useNow,
useObjectUrl,
useOffsetPagination,
useOnline,
usePageLeave,
useParallax,
useParentElement,
usePerformanceObserver,
usePermission,
usePointer,
usePointerLock,
usePointerSwipe,
usePreferredColorScheme,
usePreferredContrast,
usePreferredDark,
usePreferredLanguages,
usePreferredReducedMotion,
usePreferredReducedTransparency,
usePrevious,
useRafFn,
useRefHistory,
useResizeObserver,
useSSRWidth,
useScreenOrientation,
useScreenSafeArea,
useScriptTag,
useScroll,
useScrollLock,
useSessionStorage,
useShare,
useSorted,
useSpeechRecognition,
useSpeechSynthesis,
useStepper,
useStorage,
useStorageAsync,
useStyleTag,
useSupported,
useSwipe,
useTemplateRefsList,
useTextDirection,
useTextSelection,
useTextareaAutosize,
useThrottleFn,
useThrottledRefHistory,
useTimeAgo,
useTimeout,
useTimeoutFn,
useTimeoutPoll,
useTimestamp,
useTitle,
useToNumber,
useToString,
useToggle,
useTransition,
useUrlSearchParams,
useUserMedia,
useVModel,
useVModels,
useVibrate,
useVirtualList,
useWakeLock,
useWebNotification,
useWebSocket,
useWebWorker,
useWebWorkerFn,
useWindowFocus,
useWindowScroll,
useWindowSize,
watchArray,
watchAtMost,
watchDebounced,
watchDeep,
watchIgnorable,
watchImmediate,
watchOnce,
watchPausable,
watchThrottled,
watchTriggerable,
watchWithFilter,
whenever
} from "./chunk-EBBFFI5H.js";
import "./chunk-XZXUNI6J.js";
export {
DefaultMagicKeysAliasMap,
StorageSerializers,
TransitionPresets,
assert,
computedAsync as asyncComputed,
refAutoReset as autoResetRef,
breakpointsAntDesign,
breakpointsBootstrapV5,
breakpointsElement,
breakpointsMasterCss,
breakpointsPrimeFlex,
breakpointsQuasar,
breakpointsSematic,
breakpointsTailwind,
breakpointsVuetify,
breakpointsVuetifyV2,
breakpointsVuetifyV3,
bypassFilter,
camelize,
clamp,
cloneFnJSON,
computedAsync,
computedEager,
computedInject,
computedWithControl,
containsProp,
computedWithControl as controlledComputed,
controlledRef,
createEventHook,
createFetch,
createFilterWrapper,
createGlobalState,
createInjectionState,
reactify as createReactiveFn,
createRef,
createReusableTemplate,
createSharedComposable,
createSingletonPromise,
createTemplatePromise,
createUnrefFn,
customStorageEventName,
debounceFilter,
refDebounced as debouncedRef,
watchDebounced as debouncedWatch,
defaultDocument,
defaultLocation,
defaultNavigator,
defaultWindow,
computedEager as eagerComputed,
executeTransition,
extendRef,
formatDate,
formatTimeAgo,
get,
getLifeCycleTarget,
getSSRHandler,
hasOwn,
hyphenate,
identity,
watchIgnorable as ignorableWatch,
increaseWithUnit,
injectLocal,
invoke,
isClient,
isDef,
isDefined,
isIOS,
isObject,
isWorker,
makeDestructurable,
mapGamepadToXbox360Controller,
noop,
normalizeDate,
notNullish,
now,
objectEntries,
objectOmit,
objectPick,
onClickOutside,
onElementRemoval,
onKeyDown,
onKeyPressed,
onKeyStroke,
onKeyUp,
onLongPress,
onStartTyping,
pausableFilter,
watchPausable as pausableWatch,
promiseTimeout,
provideLocal,
provideSSRWidth,
pxValue,
rand,
reactify,
reactifyObject,
reactiveComputed,
reactiveOmit,
reactivePick,
refAutoReset,
refDebounced,
refDefault,
refThrottled,
refWithControl,
resolveRef,
resolveUnref,
set,
setSSRHandler,
syncRef,
syncRefs,
templateRef,
throttleFilter,
refThrottled as throttledRef,
watchThrottled as throttledWatch,
timestamp,
toArray,
toReactive,
toRef,
toRefs,
toValue,
tryOnBeforeMount,
tryOnBeforeUnmount,
tryOnMounted,
tryOnScopeDispose,
tryOnUnmounted,
unrefElement,
until,
useActiveElement,
useAnimate,
useArrayDifference,
useArrayEvery,
useArrayFilter,
useArrayFind,
useArrayFindIndex,
useArrayFindLast,
useArrayIncludes,
useArrayJoin,
useArrayMap,
useArrayReduce,
useArraySome,
useArrayUnique,
useAsyncQueue,
useAsyncState,
useBase64,
useBattery,
useBluetooth,
useBreakpoints,
useBroadcastChannel,
useBrowserLocation,
useCached,
useClipboard,
useClipboardItems,
useCloned,
useColorMode,
useConfirmDialog,
useCountdown,
useCounter,
useCssVar,
useCurrentElement,
useCycleList,
useDark,
useDateFormat,
refDebounced as useDebounce,
useDebounceFn,
useDebouncedRefHistory,
useDeviceMotion,
useDeviceOrientation,
useDevicePixelRatio,
useDevicesList,
useDisplayMedia,
useDocumentVisibility,
useDraggable,
useDropZone,
useElementBounding,
useElementByPoint,
useElementHover,
useElementSize,
useElementVisibility,
useEventBus,
useEventListener,
useEventSource,
useEyeDropper,
useFavicon,
useFetch,
useFileDialog,
useFileSystemAccess,
useFocus,
useFocusWithin,
useFps,
useFullscreen,
useGamepad,
useGeolocation,
useIdle,
useImage,
useInfiniteScroll,
useIntersectionObserver,
useInterval,
useIntervalFn,
useKeyModifier,
useLastChanged,
useLocalStorage,
useMagicKeys,
useManualRefHistory,
useMediaControls,
useMediaQuery,
useMemoize,
useMemory,
useMounted,
useMouse,
useMouseInElement,
useMousePressed,
useMutationObserver,
useNavigatorLanguage,
useNetwork,
useNow,
useObjectUrl,
useOffsetPagination,
useOnline,
usePageLeave,
useParallax,
useParentElement,
usePerformanceObserver,
usePermission,
usePointer,
usePointerLock,
usePointerSwipe,
usePreferredColorScheme,
usePreferredContrast,
usePreferredDark,
usePreferredLanguages,
usePreferredReducedMotion,
usePreferredReducedTransparency,
usePrevious,
useRafFn,
useRefHistory,
useResizeObserver,
useSSRWidth,
useScreenOrientation,
useScreenSafeArea,
useScriptTag,
useScroll,
useScrollLock,
useSessionStorage,
useShare,
useSorted,
useSpeechRecognition,
useSpeechSynthesis,
useStepper,
useStorage,
useStorageAsync,
useStyleTag,
useSupported,
useSwipe,
useTemplateRefsList,
useTextDirection,
useTextSelection,
useTextareaAutosize,
refThrottled as useThrottle,
useThrottleFn,
useThrottledRefHistory,
useTimeAgo,
useTimeout,
useTimeoutFn,
useTimeoutPoll,
useTimestamp,
useTitle,
useToNumber,
useToString,
useToggle,
useTransition,
useUrlSearchParams,
useUserMedia,
useVModel,
useVModels,
useVibrate,
useVirtualList,
useWakeLock,
useWebNotification,
useWebSocket,
useWebWorker,
useWebWorkerFn,
useWindowFocus,
useWindowScroll,
useWindowSize,
watchArray,
watchAtMost,
watchDebounced,
watchDeep,
watchIgnorable,
watchImmediate,
watchOnce,
watchPausable,
watchThrottled,
watchTriggerable,
watchWithFilter,
whenever
};
//# sourceMappingURL=vitepress___@vueuse_core.js.map

查看文件

@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

文件差异因一行或多行过长而隐藏

文件差异内容过多而无法显示 加载差异

文件差异因一行或多行过长而隐藏

文件差异内容过多而无法显示 加载差异

文件差异因一行或多行过长而隐藏

查看文件

@ -0,0 +1,347 @@
import {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBaseVNode,
createBlock,
createCommentVNode,
createElementBlock,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
nodeOps,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
patchProp,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId
} from "./chunk-XZXUNI6J.js";
export {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBlock,
createCommentVNode,
createElementBlock,
createBaseVNode as createElementVNode,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
nodeOps,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
patchProp,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId
};
//# sourceMappingURL=vue.js.map

查看文件

@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

查看文件

@ -21,7 +21,9 @@ export default defineConfig({
{ text: 'iOS', link: '/ios/' }, { text: 'iOS', link: '/ios/' },
{ text: 'React Native', link: '/rn/' }, { text: 'React Native', link: '/rn/' },
{ text: 'Vue3', link: '/vue3/' }, { text: 'Vue3', link: '/vue3/' },
{ text: 'Flutter', link: '/flutter/' },
{ text: 'HarmonyOS', link: '/harmony/' }, { text: 'HarmonyOS', link: '/harmony/' },
{ text: '微信小程序', link: '/miniprogram/' },
], ],
}, },
{ text: 'API 速查', link: '/server/api' }, { text: 'API 速查', link: '/server/api' },
@ -43,7 +45,6 @@ export default defineConfig({
{ text: 'IM 接入', link: '/android/im' }, { text: 'IM 接入', link: '/android/im' },
{ text: '推送接入', link: '/android/push' }, { text: '推送接入', link: '/android/push' },
{ text: '版本管理', link: '/android/update' }, { text: '版本管理', link: '/android/update' },
{ text: 'API Reference', link: '/android/api' },
], ],
'/ios/': [ '/ios/': [
{ text: '概览', link: '/ios/' }, { text: '概览', link: '/ios/' },
@ -51,7 +52,6 @@ export default defineConfig({
{ text: 'IM 接入', link: '/ios/im' }, { text: 'IM 接入', link: '/ios/im' },
{ text: '推送接入', link: '/ios/push' }, { text: '推送接入', link: '/ios/push' },
{ text: '版本管理', link: '/ios/update' }, { text: '版本管理', link: '/ios/update' },
{ text: 'API Reference', link: '/ios/api' },
], ],
'/rn/': [ '/rn/': [
{ text: '概览', link: '/rn/' }, { text: '概览', link: '/rn/' },
@ -59,18 +59,24 @@ export default defineConfig({
{ text: 'IM 接入', link: '/rn/im' }, { text: 'IM 接入', link: '/rn/im' },
{ text: '群聊', link: '/rn/group' }, { text: '群聊', link: '/rn/group' },
{ text: '版本管理', link: '/rn/update' }, { text: '版本管理', link: '/rn/update' },
{ text: 'API Reference', link: '/rn/api' },
], ],
'/vue3/': [ '/vue3/': [
{ text: '概览', link: '/vue3/' }, { text: '概览', link: '/vue3/' },
{ text: '安装配置', link: '/vue3/setup' }, { text: '安装配置', link: '/vue3/setup' },
{ text: 'IM 接入', link: '/vue3/im' }, { text: 'IM 接入', link: '/vue3/im' },
{ text: 'API Reference', link: '/vue3/api' }, ],
'/flutter/': [
{ text: '概览', link: '/flutter/' },
], ],
'/harmony/': [ '/harmony/': [
{ text: '概览', link: '/harmony/' }, { text: '概览', link: '/harmony/' },
{ text: '安装配置', link: '/harmony/setup' }, { text: '安装配置', link: '/harmony/setup' },
{ text: 'IM 接入', link: '/harmony/im' }, { text: 'IM 接入', link: '/harmony/im' },
{ text: '推送接入', link: '/harmony/#push-接入' },
{ text: '版本管理', link: '/harmony/#update-接入' },
],
'/miniprogram/': [
{ text: '概览', link: '/miniprogram/' },
], ],
'/server/': [ '/server/': [
{ text: 'API 速查', link: '/server/api' }, { text: 'API 速查', link: '/server/api' },

326
docs-site/docs/android/im.md 普通文件
查看文件

@ -0,0 +1,326 @@
# Android IM 接入
基于 `sdk-im` 模块实现即时通讯功能。
---
## 初始化详情
`Application.onCreate()` 中完成初始化,只需传入 `appKey`
```kotlin
import com.xuqm.sdk.XuqmSDK
import com.xuqm.sdk.core.LogLevel
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
XuqmSDK.initialize(
context = this,
appKey = "your_app_key",
logLevel = LogLevel.WARN, // 可选DEBUG / INFO / WARN / ERROR
)
}
}
```
> 服务器地址由 SDK 内置,**无需传 `serverUrl`**。
---
## 登录鉴权UserSig
`userSig` 由业务服务端用 `appSecret` 签发。登录只需 `userId + userSig`
```kotlin
import androidx.lifecycle.lifecycleScope
import com.xuqm.sdk.XuqmSDK
import kotlinx.coroutines.launch
lifecycleScope.launch {
XuqmSDK.login(
userId = "user_001",
userSig = "your_user_sig_jwt",
)
}
```
> 登录成功后,IM 模块会自动连接 WebSocket,Push 模块会自动注册设备 Token。
---
## 单聊消息收发
### 监听实时消息
```kotlin
import com.xuqm.sdk.im.ImSDK
import com.xuqm.sdk.im.listener.ImEventListener
import com.xuqm.sdk.im.model.ImMessage
ImSDK.addListener(object : ImEventListener {
override fun onConnected() { /* WebSocket 已连接 */ }
override fun onMessage(msg: ImMessage) { /* 单聊消息 */ }
override fun onGroupMessage(msg: ImMessage) { /* 群聊消息 */ }
override fun onRead(msg: ImMessage) { /* 对方已读回执 */ }
override fun onRevoke(msg: ImMessage) { /* 消息被撤回 */ }
override fun onDisconnected(reason: String?) { /* 断线处理 */ }
override fun onError(error: String) { /* 错误回调 */ }
})
```
### 发送文本消息
```kotlin
ImSDK.sendTextMessage(
toId = "user_002",
chatType = "SINGLE",
content = "Hello!",
)
```
### 发送多媒体消息
```kotlin
// 图片(需先调用 FileSDK 上传)
ImSDK.sendImageMessage(
toId = "user_002",
chatType = "SINGLE",
file = uploadResult,
width = 800,
height = 600,
)
// 视频
ImSDK.sendVideoMessage(
toId = "user_002",
chatType = "SINGLE",
file = uploadResult,
width = 1920,
height = 1080,
durationMs = 15_000,
)
// 语音
ImSDK.sendAudioMessage(
toId = "user_002",
chatType = "SINGLE",
file = uploadResult,
durationMs = 5_000,
)
// 文件
ImSDK.sendFileMessage(
toId = "user_002",
chatType = "SINGLE",
file = uploadResult,
)
// 位置
ImSDK.sendLocationMessage(
toId = "user_002",
chatType = "SINGLE",
latitude = 31.2304,
longitude = 121.4737,
title = "上海",
address = "上海市黄浦区",
)
```
### 获取历史消息
```kotlin
lifecycleScope.launch {
val history = ImSDK.fetchHistory("user_002", page = 0, size = 20)
// 带筛选的历史消息
val filtered = ImSDK.fetchHistoryWithFilters(
toId = "user_002",
page = 0,
size = 20,
msgType = "TEXT",
keyword = "会议",
)
}
```
### 撤回与编辑
```kotlin
lifecycleScope.launch {
ImSDK.revokeMessage(messageId)
ImSDK.editMessage(messageId, "新内容")
}
```
---
## 群聊
### 创建群组
```kotlin
lifecycleScope.launch {
val group = ImSDK.createGroup(
name = "项目讨论",
memberIds = listOf("user_002", "user_003"),
groupType = "WORK", // 可选WORK / PUBLIC / PRIVATE
)
}
```
### 订阅群消息
```kotlin
ImSDK.subscribeGroup("group_xxx")
```
### 发送群消息
```kotlin
ImSDK.sendTextMessage(
toId = "group_xxx",
chatType = "GROUP",
content = "大家好",
)
```
### 群成员管理
```kotlin
lifecycleScope.launch {
// 添加成员
ImSDK.addGroupMember("group_xxx", "user_004")
// 移除成员
ImSDK.removeGroupMember("group_xxx", "user_004")
// 批量添加/移除
ImSDK.batchAddGroupMembers("group_xxx", listOf("user_005", "user_006"))
ImSDK.batchRemoveGroupMembers("group_xxx", listOf("user_005", "user_006"))
// 设置角色
ImSDK.setGroupRole("group_xxx", "user_004", "ADMIN")
// 禁言
ImSDK.muteGroupMember("group_xxx", "user_004", minutes = 60)
// 转让群主
ImSDK.transferGroupOwner("group_xxx", "user_002")
// 退出群聊
ImSDK.leaveGroup("group_xxx")
// 解散群聊
ImSDK.dismissGroup("group_xxx")
}
```
### 群信息查询
```kotlin
lifecycleScope.launch {
val groups = ImSDK.listGroups()
val publicGroups = ImSDK.listPublicGroups(keyword = "项目")
val group = ImSDK.getGroupInfo("group_xxx")
val members = ImSDK.listGroupMembers("group_xxx")
val history = ImSDK.fetchGroupHistory("group_xxx", page = 0, size = 20)
}
```
---
## 好友管理
```kotlin
lifecycleScope.launch {
// 好友列表
val friends = ImSDK.listFriends()
// 添加好友
ImSDK.addFriend("user_002")
// 删除好友
ImSDK.removeFriend("user_002")
// 批量操作
ImSDK.batchAddFriends(listOf("user_002", "user_003"))
ImSDK.batchRemoveFriends(listOf("user_002", "user_003"))
// 好友分组
ImSDK.setFriendGroup("user_002", "同事")
val groups = ImSDK.listFriendGroups()
// 好友请求
ImSDK.sendFriendRequest("user_002", remark = "你好")
val requests = ImSDK.listFriendRequests(direction = "incoming")
ImSDK.acceptFriendRequest("request_id")
ImSDK.rejectFriendRequest("request_id")
// 黑名单
ImSDK.addToBlacklist("user_002")
ImSDK.removeFromBlacklist("user_002")
val check = ImSDK.checkBlacklist("user_002")
}
```
---
## 会话列表
```kotlin
lifecycleScope.launch {
val conversations = ImSDK.listConversations()
// 置顶
ImSDK.setConversationPinned("user_002", "SINGLE", true)
// 免打扰
ImSDK.setConversationMuted("user_002", "SINGLE", true)
// 标记已读
ImSDK.markRead("user_002", "SINGLE")
// 草稿
ImSDK.setDraft("user_002", "SINGLE", "草稿内容")
// 删除会话
ImSDK.deleteConversation("user_002", "SINGLE")
// 总未读数
val totalUnread = ImSDK.getTotalUnreadCount()
}
```
---
## 离线消息同步
```kotlin
lifecycleScope.launch {
// 查询离线消息数量
val count = ImSDK.offlineMessageCount()
// 同步离线消息
val offlineMessages = ImSDK.syncOfflineMessages()
}
```
---
## 连接状态监听
```kotlin
lifecycleScope.launch {
ImSDK.connectionState.collect { state ->
when (state) {
is ImConnectionState.Connected -> { /* 已连接 */ }
is ImConnectionState.Connecting -> { /* 连接中 */ }
is ImConnectionState.Disconnected -> { /* 已断开state.reason */ }
}
}
}
```
---
## 消息类型
| MsgType | 说明 | content 结构 |
|---------|------|-------------|
| `TEXT` | 纯文本 | `String` |
| `IMAGE` | 图片 | `{url, width, height, thumbnailUrl?}` |
| `VIDEO` | 视频 | `{url, duration, thumbnailUrl, size}` |
| `AUDIO` | 语音 | `{url, duration, size}` |
| `FILE` | 文件 | `{url, name, size, mimeType}` |
| `LOCATION` | 位置 | `{lat, lng, address, title}` |
| `CUSTOM` | 自定义 | 任意 JSON |
| `NOTIFY` | 系统通知 | `{title, content}` |
| `RICH_TEXT` | 富文本 | `{html}` |
| `CALL_AUDIO` | 语音通话信令 | `{action}` |
| `CALL_VIDEO` | 视频通话信令 | `{action}` |
| `FORWARD` | 转发 | `{originalSender, originalContent}` |
| `QUOTE` | 引用 | `{quotedMsgId, quotedContent, text}` |
| `MERGE` | 合并转发 | `{title, msgList}` |
| `REVOKED` | 撤回 | 系统内部填充 |
[→ 完整 API Reference](./api)

查看文件

@ -0,0 +1,169 @@
# Android 推送接入指南
**模块**`com.xuqm:sdk-push` · **支持厂商**华为、小米、OPPO、vivo、荣耀、FCM
SDK 在 `XuqmSDK.login()` 成功后会自动检测手机厂商、初始化对应 Push SDK、获取 Token 并上报到 Push 服务端。业务层**无需手动调用**任何 Push 注册 API。
如需主动控制,可参考以下独立接入方式。
---
## 1. 添加依赖
```kotlin
// app/build.gradle.kts
dependencies {
implementation("com.xuqm:sdk-push:0.4.0")
}
```
各厂商需要额外添加对应 Push SDK 依赖(按需):
| 厂商 | 依赖 |
|------|------|
| 华为 | `com.huawei.hms:push:6.x.x.xxx` |
| 小米 | `com.xiaomi.mipush:mipush:5.x.x` |
| OPPO | `com.heytap.mcs:push:3.x.x` |
| vivo | `com.vivo.pushsdk:pushsdk:3.x.x` |
| 荣耀 | `com.hihonor.mcs:push:7.x.x.xxx` |
| FCM | `com.google.firebase:firebase-messaging` |
---
## 2. AndroidManifest.xml 配置
`<application>` 下添加各厂商对应的 `meta-data`
```xml
<!-- 小米 -->
<meta-data android:name="XUQM_XIAOMI_APP_ID"
android:value="288230376xxxxxxxx" />
<meta-data android:name="XUQM_XIAOMI_APP_KEY"
android:value="xxxxxxxxxxxx" />
<!-- OPPO -->
<meta-data android:name="XUQM_OPPO_APP_KEY"
android:value="xxxxxxxx" />
<meta-data android:name="XUQM_OPPO_APP_SECRET"
android:value="xxxxxxxx" />
```
> 华为、荣耀、vivo、FCM 无需在 `AndroidManifest.xml` 中配置 `meta-data`,其配置方式如下:
> - **华为**:通过 `agconnect-services.json` 自动读取 `app_id`
> - **荣耀**:在 `Application.onCreate()` 中调用 `HonorPushClient.getInstance().init(context, true)`
> - **vivo**:在 `Application.onCreate()` 中调用 `PushClient.getInstance(context).initialize()`
> - **FCM**:通过 `google-services.json` 自动集成
---
## 3. 初始化
`Application.onCreate()` 中调用:
```kotlin
PushSDK.initializeVendors(context)
```
SDK 会自动检测当前设备厂商(`HUAWEI / XIAOMI / OPPO / VIVO / HONOR / FCM`),并初始化对应的推送服务。
如需查询当前检测到的厂商:
```kotlin
val vendor = PushSDK.detectVendor()
// 返回HUAWEI / XIAOMI / OPPO / VIVO / HONOR / FCM
```
---
## 4. 手动上报 Token
如果业务层自定义了厂商推送服务(如自定义 `HmsMessageService`、`MiPushMessageReceiver` 等),需在收到 Token 回调后手动上报:
```kotlin
// 华为 HMS 示例
class MyHmsService : HmsMessageService() {
override fun onNewToken(token: String) {
super.onNewToken(token)
PushSDK.updateNativePushToken(context, PushVendor.HUAWEI, token)
}
}
```
```kotlin
// 小米示例
class MyMiPushReceiver : PushMessageReceiver() {
override fun onReceiveRegisterResult(context: Context, miPushCommandMessage: MiPushCommandMessage) {
val token = miPushCommandMessage.commandArguments?.getOrNull(0)
if (!token.isNullOrBlank()) {
PushSDK.updateNativePushToken(context, PushVendor.XIAOMI, token)
}
}
}
```
```kotlin
// OPPO 示例(在 PushCallback.onRegister 中)
PushSDK.updateNativePushToken(context, PushVendor.OPPO, regId)
```
```kotlin
// vivo 示例(在 OpenClientPushMessageReceiver.onReceiveRegId 中)
PushSDK.updateNativePushToken(context, PushVendor.VIVO, regId)
```
```kotlin
// 荣耀示例(在 HonorMessageService.onNewToken 中)
PushSDK.updateNativePushToken(context, PushVendor.HONOR, token)
```
```kotlin
// FCM 示例(在 FirebaseMessagingService.onNewToken 中)
class MyFcmService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
super.onNewToken(token)
PushSDK.updateNativePushToken(this, PushVendor.FCM, token)
}
}
```
---
## 5. 接收推送消息
各厂商推送消息通过各自的 SDK 回调接收,SDK 不负责解析消息内容,业务层需自行实现:
- **华为**`HmsMessageService.onMessageReceived(RemoteMessage)`
- **小米**`PushMessageReceiver.onReceivePassThroughMessage()` / `onNotificationMessageClicked()`
- **OPPO**`PushCallback.onGetPushMessage()`
- **vivo**`OpenClientPushMessageReceiver.onTransmissionMessage()`
- **荣耀**`HonorMessageService.onMessageReceived()`
- **FCM**`FirebaseMessagingService.onMessageReceived(RemoteMessage)`
> 建议在收到推送消息后,调用业务层的通知管理器展示本地通知,或跳转至对应会话页面。
---
## 6. 开启或关闭推送
```kotlin
// 关闭推送接收
PushSDK.setReceivePush(context, enabled = false)
// 开启推送接收
PushSDK.setReceivePush(context, enabled = true)
```
---
## 7. 多模块统一登录
Push 模块与 IM、Update 模块共享同一套登录态:
```kotlin
// 登录成功后自动触发
XuqmSDK.login(userId = "user_001", userSig = "jwt_token")
// ↓ 自动触发
// · PushSDK.onSdkLogin → 初始化厂商 Push 并注册 Token
```
无需业务层手动调用 `PushSDK.initializeVendors()``bindImUser()`

查看文件

@ -0,0 +1,84 @@
# Android 安装配置
**版本**0.5.x · **最低 Android 版本**API 24 (Android 7.0) · **语言**Kotlin
---
## Gradle 仓库配置
`settings.gradle.kts` 中添加 XuqmGroup Maven 仓库:
```kotlin
dependencyResolutionManagement {
repositories {
maven("https://nexus.xuqinmin.com/repository/android/")
google()
mavenCentral()
}
}
```
---
## 各模块 Artifact 和版本
| 模块 | Artifact | 功能 |
|------|----------|------|
| sdk-core | `com.xuqm:sdk-core` | 初始化、网络、鉴权、Token 存储 |
| sdk-im | `com.xuqm:sdk-im` | 单聊、群聊、消息收发、会话、好友、群组 |
| sdk-push | `com.xuqm:sdk-push` | 自动检测厂商、设备 Token 注册(华为/小米/OPPO/vivo/荣耀/FCM|
| sdk-update | `com.xuqm:sdk-update` | App 更新检查、下载安装 |
`app/build.gradle.kts` 中按需引入:
```kotlin
dependencies {
implementation("com.xuqm:sdk-core:0.4.0")
implementation("com.xuqm:sdk-im:0.4.0")
implementation("com.xuqm:sdk-push:0.4.0") // 按需
implementation("com.xuqm:sdk-update:0.4.0") // 按需
}
```
---
## 最低 Android API 版本
- `minSdk = 24`Android 7.0
- `compileSdk` 与项目保持一致(建议 34+
- Java 11 / Kotlin 1.9+
---
## ProGuard / R8 混淆规则
各模块已自带 `consumer-rules.pro`,业务方**无需额外配置**。如需手动维护,可添加:
```proguard
# XuqmSDK Core
-keep class com.xuqm.sdk.** { *; }
-keepclassmembers class com.xuqm.sdk.** { *; }
# XuqmSDK IM
-keep class com.xuqm.sdk.im.** { *; }
-keepclassmembers class com.xuqm.sdk.im.** { *; }
# XuqmSDK Push
-keep class com.xuqm.sdk.push.** { *; }
# XuqmSDK Update
-keep class com.xuqm.sdk.update.** { *; }
# GsonIM 消息序列化使用)
-keepattributes Signature
-keepattributes *Annotation*
-keep class com.google.gson.** { *; }
```
---
## 下一步
- [Android IM 接入 →](./im)
- [Android Push 接入 →](./push)
- [Android 版本更新 →](./update)

查看文件

@ -0,0 +1,119 @@
# Android 版本更新接入指南
**模块**`com.xuqm:sdk-update` · **功能**App 版本检查、下载安装
---
## 1. 添加依赖
```kotlin
// app/build.gradle.kts
dependencies {
implementation("com.xuqm:sdk-update:0.4.0")
}
```
---
## 2. 检查更新
```kotlin
import com.xuqm.sdk.update.UpdateSDK
// 在协程中调用
lifecycleScope.launch {
val update = UpdateSDK.checkAppUpdate(context)
if (update?.needsUpdate == true) {
println("发现新版本: ${update.versionName}")
println("更新日志: ${update.changeLog}")
println("下载地址: ${update.downloadUrl}")
if (update.forceUpdate == true) {
// 强制更新:必须升级后才能使用
showForceUpdateDialog(update)
} else {
// 可选更新:提示用户
showOptionalUpdateDialog(update)
}
}
}
```
---
## 3. 下载并安装
```kotlin
lifecycleScope.launch {
UpdateSDK.downloadAndInstall(context, downloadUrl) { progress ->
// 更新下载进度 0-100
updateProgressBar(progress)
}
}
```
`downloadAndInstall` 内部会:
1. 下载 APK 到外部存储目录
2. 通过 `FileProvider` 获取文件 URI
3. 启动系统安装界面
**需提前配置 `FileProvider`**
```xml
<!-- AndroidManifest.xml -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
```
```xml
<!-- res/xml/file_paths.xml -->
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path name="updates" path="." />
</paths>
```
---
## 4. 强制更新处理
当服务端返回 `forceUpdate = true` 时,建议业务层:
1. 弹出不可取消的对话框
2. 只允许用户点击「立即更新」
3. 下载完成后自动调用安装
```kotlin
fun showForceUpdateDialog(update: UpdateInfo) {
AlertDialog.Builder(context)
.setTitle("发现重要更新")
.setMessage("当前版本已不可用,请升级至 ${update.versionName}")
.setCancelable(false)
.setPositiveButton("立即更新") { _, _ ->
lifecycleScope.launch {
UpdateSDK.downloadAndInstall(context, update.downloadUrl!!) { progress ->
// 显示下载进度
}
}
}
.show()
}
```
---
## 5. 多模块统一登录
Update 模块与 IM、Push 模块共享同一套登录态:
```kotlin
XuqmSDK.login(userId = "user_001", userSig = "jwt_token")
// UpdateSDK 在 checkAppUpdate 时自动携带 appKey,无需额外登录操作
```

查看文件

@ -0,0 +1,333 @@
# Flutter SDK 概览
**包名**`xuqm_flutter_sdk` · **版本**0.2.x · **语言**Dart
`xuqm_flutter_sdk` 是 XuqmGroup 的 Flutter 端统一入口,通过 MethodChannel 桥接原生能力,同时提供纯 Dart 实现的 IM HTTP API。
---
## 功能模块
| 模块 | 包 | 功能 |
|------|-----|------|
| `xuqm_flutter_common` | `packages/common` | 初始化、网络、配置管理 |
| `xuqm_flutter_im` | `packages/im` | 单聊、群聊、消息收发、会话、好友、群组 |
| `xuqm_flutter_push` | `packages/push` | 设备 Token 注册、厂商检测Android/ APNsiOS|
| `xuqm_flutter_update` | `packages/update` | App 版本检查、商店跳转、APK 下载Android|
---
## 安装
`pubspec.yaml` 中添加:
```yaml
dependencies:
xuqm_flutter_sdk: ^0.2.0
```
配置私有仓库(如有):
```yaml
dependency_overrides:
xuqm_flutter_common:
hosted:
name: xuqm_flutter_common
url: https://nexus.xuqinmin.com/repository/pub-hosted/
version: ^0.2.0
```
执行安装:
```bash
flutter pub get
```
---
## 快速接入
### 1. 初始化
```dart
import 'package:xuqm_flutter_sdk/xuqm_flutter_sdk.dart';
await XuqmSDK.initialize(XuqmInitOptions(
appKey: 'your_app_key', // 在租户平台创建应用后获得
debug: true, // 可选,开启详细日志
));
```
初始化时会自动向服务端请求远程配置IM API 地址等),若网络异常则回退到内置默认值。
如需同步初始化(不拉取远程配置):
```dart
XuqmSDK.init(XuqmInitOptions(appKey: 'your_app_key'));
```
---
### 2. IM 登录
```dart
import 'package:xuqm_flutter_sdk/xuqm_flutter_sdk.dart';
// 只需要 userId + userSig
await XuqmImSdk().login('user_001', 'your_user_sig_jwt');
```
登录成功后会自动建立 WebSocket 连接。
---
### 3. 监听消息
```dart
final im = XuqmImSdk();
im.ws.onConnected = () {
print('WebSocket 已连接');
};
im.ws.onMessage = (XuqmImMessage msg) {
print('收到消息: ${msg.msgType} - ${msg.content}');
};
im.ws.onDisconnected = (String? reason) {
print('WebSocket 断开: $reason');
};
```
---
### 4. 发送消息
```dart
// 发送文本消息
final msg = await im.sendTextMessage(
'user_002',
'SINGLE',
'Hello from Flutter!',
);
// 发送图片消息
final imgMsg = await im.sendMessage(
'user_002',
'SINGLE',
'IMAGE',
jsonEncode({'url': 'https://cdn.example.com/img.jpg', 'width': 800, 'height': 600}),
);
// 撤回消息
await im.revokeMessage(msg.id);
// 编辑消息
await im.editMessage(msg.id, '新内容');
```
---
### 5. 会话管理
```dart
// 会话列表
final conversations = await im.listConversations();
// 置顶会话
await im.setConversationPinned('user_002', 'SINGLE', true);
// 免打扰
await im.setConversationMuted('group_xxx', 'GROUP', true);
// 标记已读
await im.markRead('user_002');
// 设置草稿
await im.setDraft('user_002', 'SINGLE', '未完成的消息');
```
---
### 6. 好友与群组
```dart
// 好友列表
final friends = await im.listFriends();
// 添加好友
await im.addFriend('user_002');
// 移除好友
await im.removeFriend('user_002');
// 创建群组
final group = await im.createGroup('Flutter 群', ['user_001', 'user_002']);
// 群组列表
final groups = await im.listGroups();
// 添加群成员
await im.addGroupMember(group.id, 'user_003');
// 退出群聊
await im.leaveGroup(group.id);
```
---
### 7. 历史消息
```dart
// 单聊历史
final history = await im.fetchHistory('user_002', page: 0, size: 20);
// 群聊历史
final groupHistory = await im.fetchGroupHistory('group_xxx', page: 0, size: 50);
// 定位消息所在页
final page = await im.locateHistoryPage(
'user_002',
messageId: 'msg_xxx',
pageSize: 20,
);
```
---
## Push 接入
```dart
import 'package:xuqm_flutter_sdk/xuqm_flutter_sdk.dart';
// 检测当前设备推送厂商Android
final vendor = await XuqmPushSdk.detectVendor();
// 返回HUAWEI / XIAOMI / OPPO / VIVO / HONOR / FCM / APNS
// 请求原生推送注册
await XuqmPushSdk.requestNativeRegistration();
// 监听原生 Token 回调
XuqmPushSdk.onPushToken.listen((tokenInfo) {
final token = tokenInfo['token'];
final vendor = tokenInfo['vendor'];
print('收到 Push Token: $token, 厂商: $vendor');
// 手动上报到服务端(如需要)
final pushSdk = XuqmPushSdk();
await pushSdk.registerToken(
'user_001',
token!,
vendor: vendor,
platform: 'ANDROID',
);
});
```
---
## Update 接入
```dart
import 'package:xuqm_flutter_sdk/xuqm_flutter_sdk.dart';
final updateSdk = XuqmUpdateSdk();
// 检查 App 更新
final update = await updateSdk.checkAppUpdate();
if (update.needsUpdate) {
print('新版本: ${update.versionName}');
print('更新日志: ${update.changeLog}');
if (update.forceUpdate) {
// 强制更新
showForceUpdateDialog(update);
} else {
// 可选更新
showOptionalUpdateDialog(update);
}
}
// 打开商店 / 应用市场
await updateSdk.openStore(update);
// Android直接打开 APK 下载链接
await updateSdk.openDownloadUrl(update);
```
开发/测试环境可覆盖版本号:
```dart
XuqmUpdateSdk.devSetAppVersion(100, '1.0.0');
```
---
## 多模块统一登录
无论集成了哪些模块IM、Push、Update,**初始化和登录永远只做一次**
```dart
// 初始化
await XuqmSDK.initialize(XuqmInitOptions(appKey: 'your_app_key'));
// 登录(业务登录成功后调用一次)
await XuqmImSdk().login('user_001', 'your_user_sig_jwt');
// 登出
await XuqmImSdk().logout();
```
---
## 完整示例
```dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:xuqm_flutter_sdk/xuqm_flutter_sdk.dart';
class ChatPage extends StatefulWidget {
@override
_ChatPageState createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
final im = XuqmImSdk();
final List<XuqmImMessage> messages = [];
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
await XuqmSDK.initialize(XuqmInitOptions(appKey: 'your_app_key'));
await im.login('user_001', 'your_user_sig_jwt');
im.ws.onMessage = (msg) {
setState(() => messages.add(msg));
};
}
Future<void> _send(String text) async {
final msg = await im.sendTextMessage('user_002', 'SINGLE', text);
setState(() => messages.add(msg));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Flutter IM')),
body: ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) {
final msg = messages[index];
return ListTile(title: Text('${msg.fromId}: ${msg.content}'));
},
),
);
}
}
```

查看文件

@ -0,0 +1,114 @@
# 平台概念
了解 XuqmGroup 平台的核心概念,有助于更好地接入 SDK。
---
## App / Tenant
| 概念 | 说明 |
|------|------|
| **Tenant租户** | 对应一个开发者账号/企业,可在控制台创建多个应用 |
| **App应用** | 对应一个具体的客户端应用,拥有独立的 `appKey``appSecret` |
| **appKey** | 应用唯一标识,客户端初始化时传入 |
| **appSecret** | 应用密钥,**仅保存在服务端**,用于签发 UserSig |
---
## UserSig
UserSig 是 XuqmGroup 的登录鉴权凭证,由业务服务端用 `appSecret` 为用户签发的安全凭证。
### 特点
- 当前版本不过期,只校验 `userId + UserSig` 是否匹配
- `appSecret` **绝不下发到客户端**
- 若需撤销权限,可在租户平台重置 `appSecret` 或拉黑账号
### 签发方式(示例)
```ts
// Node.js 示例
import jwt from 'jsonwebtoken'
const userSig = jwt.sign(
{ userId: 'user_001', appKey: 'your_app_key' },
'your_app_secret',
{ algorithm: 'HS256' }
)
```
---
## 消息类型
| 类型 | 说明 | content 结构 |
|------|------|-------------|
| TEXT | 纯文本 | `String` |
| IMAGE | 图片 | `{url, width, height, thumbnailUrl?}` |
| VIDEO | 视频 | `{url, duration, thumbnailUrl, size}` |
| AUDIO | 语音 | `{url, duration, size}` |
| FILE | 文件 | `{url, name, size, mimeType}` |
| LOCATION | 位置 | `{lat, lng, address, title}` |
| CUSTOM | 自定义 | 任意 JSON |
| NOTIFY | 系统通知 | `{title, content}` |
| RICH_TEXT | 富文本 | `{html}` |
| CALL_AUDIO | 语音通话信令 | `{action}` |
| CALL_VIDEO | 视频通话信令 | `{action}` |
| QUOTE | 引用 | `{quotedMsgId, quotedContent, text}` |
| MERGE | 合并转发 | `{title, msgList}` |
| FORWARD | 转发 | `{originalSender, originalContent}` |
| REVOKED | 撤回 | 系统内部填充 |
---
## 会话
会话Conversation是用户与单聊对象或群组的聊天关系抽象。
| 属性 | 说明 |
|------|------|
| targetId | 对方用户 ID 或群 ID |
| chatType | `SINGLE` / `GROUP` |
| unreadCount | 未读消息数 |
| isPinned | 是否置顶 |
| isMuted | 是否免打扰 |
| lastMsgContent | 最后一条消息内容 |
| lastMsgTime | 最后一条消息时间Unix 毫秒)|
---
## 群组
| 属性 | 说明 |
|------|------|
| id | 群唯一 ID |
| name | 群名称 |
| creatorId | 创建者用户 ID |
| memberIds | 成员列表JSON 数组字符串)|
| adminIds | 管理员列表JSON 数组字符串)|
| groupType | 群类型:`WORK` / `PUBLIC` / `PRIVATE` |
| announcement | 群公告 |
### 群角色
| 角色 | 说明 |
|------|------|
| OWNER | 群主 |
| ADMIN | 管理员 |
| MEMBER | 普通成员 |
---
## 消息状态
| 状态 | 说明 |
|------|------|
| SENDING | 发送中 |
| SENT | 已发送 |
| DELIVERED | 已送达 |
| READ | 已读 |
| FAILED | 发送失败 |
| REVOKED | 已撤回 |
[→ 接入流程 →](./flow)

137
docs-site/docs/guide/flow.md 普通文件
查看文件

@ -0,0 +1,137 @@
# 接入流程
完整的 XuqmGroup SDK 接入流程,从注册账号到客户端收发消息。
---
## 1. 注册开发者账号
1. 访问 [XuqmGroup 控制台](https://dev.xuqinmin.com)
2. 点击注册,填写企业/个人信息
3. 完成邮箱/手机验证
---
## 2. 创建应用
1. 登录控制台 → 应用管理 → 创建应用
2. 填写应用名称、平台类型Android / iOS / Web / RN / 小程序 / HarmonyOS
3. 创建成功后获得:
- `appKey`(客户端使用)
- `appSecret`(服务端使用,**不可泄露**
---
## 3. 获取 AppKey
在应用详情页复制 `appKey`,用于客户端 SDK 初始化:
```kotlin
// Android
XuqmSDK.initialize(context, appKey = "your_app_key")
```
```swift
// iOS
let config = SDKConfig(appKey: "your_app_key", appSecret: "your_app_secret")
XuqmSDK.shared.initialize(config: config)
```
```ts
// Vue3 / Web
init({ appKey: 'your_app_key', appSecret: 'your_app_secret' })
```
---
## 4. 服务端签发 UserSig
`appSecret` 只应保存在业务服务端,用于为每个用户签发 `userSig`
### 签发逻辑(示例)
```ts
// Node.js
import jwt from 'jsonwebtoken'
function generateUserSig(userId: string, appKey: string, appSecret: string): string {
return jwt.sign(
{ userId, appKey },
appSecret,
{ algorithm: 'HS256' }
)
}
```
```python
# Python
import jwt
def generate_user_sig(user_id: str, app_key: str, app_secret: str) -> str:
return jwt.encode(
{"userId": user_id, "appKey": app_key},
app_secret,
algorithm="HS256"
)
```
```go
// Go
import "github.com/golang-jwt/jwt/v5"
func GenerateUserSig(userID, appKey, appSecret string) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"userId": userID,
"appKey": appKey,
})
return token.SignedString([]byte(appSecret))
}
```
### 接口设计建议
```
POST /api/auth/xuqm-login
Headers: Authorization: Bearer {your-app-auth-token}
Body: { "userId": "user_001" }
Response: { "userSig": "jwt_token_string" }
```
---
## 5. 客户端接入 SDK
### 登录流程
```
客户端
→ 业务登录(用户名/密码)
→ 业务服务端验证成功
→ 业务服务端调用 generateUserSig(userId, appKey, appSecret)
→ 返回 userSig 给客户端
→ 客户端调用 XuqmSDK.login(userId, userSig)
→ SDK 自动连接 WebSocket
→ 开始收发消息
```
### 各平台接入
| 平台 | 文档 |
|------|------|
| Android | [Android SDK →](/android/) |
| iOS | [iOS SDK →](/ios/) |
| React Native | [RN SDK →](/rn/) |
| Vue3 / Web | [Vue3 SDK →](/vue3/) |
| HarmonyOS | [HarmonyOS SDK →](/harmony/) |
| 微信小程序 | [小程序 SDK →](/miniprogram/) |
---
## 安全提示
- `appSecret` **绝不下发到客户端**,仅用于服务端签发 UserSig
- 若需撤销用户权限,可在租户平台重置 `appSecret` 或拉黑账号
- 所有 API 通信使用 HTTPS / WSS
- UserSig 当前版本不过期,业务方可自行控制签发逻辑
[→ 快速开始 →](./quickstart)

194
docs-site/docs/harmony/im.md 普通文件
查看文件

@ -0,0 +1,194 @@
# HarmonyOS IM 接入
基于 `@xuqm/harmony-sdk` 实现即时通讯功能。
---
## 初始化与登录
### 初始化
```ts
import { XuqmSDK } from '@xuqm/harmony-sdk'
import common from '@ohos.app.ability.common'
const context = getContext(this) as common.UIAbilityContext
await XuqmSDK.init(context, {
appKey: 'your_app_key',
appSecret: 'your_app_secret',
})
```
### 登录
```ts
const session = await XuqmSDK.login('user_001', 'your_user_sig_jwt')
```
### 登出
```ts
await XuqmSDK.logout()
```
---
## 消息收发
### 设置事件代理
```ts
const im = XuqmSDK.im
im.delegate = {
onConnected: () => {
console.log('IM 已连接')
},
onDisconnected: (code: number, reason: string) => {
console.log('断开连接:', code, reason)
},
onMessage: (msg: ImMessage) => {
console.log('收到消息:', msg.msgType, msg.content)
},
onRead: (msg: ImMessage) => {
console.log('已读回执:', msg.id)
},
onRevoke: (data: RevokeData) => {
console.log('消息被撤回:', data.msgId)
},
onError: (message: string) => {
console.error('IM 错误:', message)
},
}
im.connect()
```
### 发送消息
```ts
const outgoing = im.send({
toId: 'user_002',
chatType: 'SINGLE',
msgType: 'TEXT',
content: 'Hello!',
})
```
### 发送多媒体消息
```ts
// 图片
im.send({
toId: 'user_002',
chatType: 'SINGLE',
msgType: 'IMAGE',
content: JSON.stringify({ url: 'https://...', width: 800, height: 600 }),
})
// 位置
im.send({
toId: 'user_002',
chatType: 'SINGLE',
msgType: 'LOCATION',
content: JSON.stringify({ lat: 31.2304, lng: 121.4737, title: '上海' }),
})
```
### 撤回消息
```ts
im.revoke('message_id')
```
### 获取历史消息
```ts
const result = await im.fetchHistory('user_002', 0, 20)
// result.content — 消息列表
// result.totalElements — 总数量
const groupResult = await im.fetchGroupHistory('group_xxx', 0, 50)
```
---
## 会话列表
```ts
const conversations = await im.listConversations(20)
// 标记已读
await im.markRead('user_002', 'SINGLE')
// 草稿
await im.setDraft('user_002', 'SINGLE', '草稿内容')
// 置顶
await im.setConversationPinned('user_002', 'SINGLE', true)
// 免打扰
await im.setConversationMuted('user_002', 'SINGLE', true)
// 删除会话
await im.deleteConversation('user_002', 'SINGLE')
```
---
## 群聊
```ts
// 创建群
const group = await im.createGroup('项目讨论', ['user_002', 'user_003'], 'WORK')
// 群列表
const groups = await im.listGroups()
// 群详情
const groupInfo = await im.getGroupInfo('group_xxx')
// 群成员
const members = await im.listGroupMembers('group_xxx')
// 添加/移除成员
await im.addGroupMember('group_xxx', 'user_004')
await im.removeGroupMember('group_xxx', 'user_004')
// 退出群聊
await im.leaveGroup('group_xxx')
```
---
## 好友管理
```ts
const friends = await im.listFriends()
await im.addFriend('user_002')
await im.removeFriend('user_002')
```
---
## 消息类型
| MsgType | 说明 |
|---------|------|
| `TEXT` | 纯文本 |
| `IMAGE` | 图片 |
| `VIDEO` | 视频 |
| `AUDIO` | 语音 |
| `FILE` | 文件 |
| `LOCATION` | 位置 |
| `CUSTOM` | 自定义 |
| `NOTIFY` | 系统通知 |
| `RICH_TEXT` | 富文本 |
| `CALL_AUDIO` | 语音通话信令 |
| `CALL_VIDEO` | 视频通话信令 |
| `QUOTE` | 引用 |
| `MERGE` | 合并转发 |
| `FORWARD` | 转发 |
| `REVOKED` | 撤回 |
[→ 完整 API Reference](./api)

查看文件

@ -22,6 +22,70 @@
} }
``` ```
---
## 8. Push 接入
HarmonyOS SDK 提供 `PushSDK` 用于注册和注销推送 Token。业务层需在获取到 HarmonyOS 推送服务的 Token 后调用注册接口。
```typescript
import { PushSDK } from '@xuqm/harmony-sdk'
// 注册 Push Token在获取到系统推送 Token 后调用)
await PushSDK.registerToken('harmony_push_token', 'user_001')
// 登出时注销 Token
await PushSDK.unregisterToken('harmony_push_token')
```
> HarmonyOS 推送 Token 通过系统 Push Kit 获取,具体接入请参考 HarmonyOS 官方推送文档。`vendor` 固定为 `HARMONY`,`platform` 固定为 `harmony`
---
## 9. 版本更新
HarmonyOS SDK 提供整包检查和 RN Bundle 热更新能力。
### 9.1 检查 App 更新
```typescript
import { UpdateSDK } from '@xuqm/harmony-sdk'
const result = await UpdateSDK.checkAppUpdate('your_app_key')
if (result.hasUpdate) {
console.log('发现新版本:', result.info?.latestVersionCode)
// HarmonyOS 整包更新只提供应用市场跳转
if (result.info?.marketUrl) {
await UpdateSDK.openAppMarket(getContext(this), result.info.marketUrl)
}
}
```
### 9.2 RN Bundle 热更新
```typescript
const rnResult = await UpdateSDK.checkRNUpdate(
'your_app_key',
'home', // bundle 名称
1 // 当前 bundle 版本号
)
if (rnResult.hasUpdate) {
console.log('发现 RN Bundle 更新:', rnResult.info?.bundleVersion)
// 下载 Bundle
const destPath = await UpdateSDK.downloadRnBundle(
getContext(this),
rnResult.info!.downloadUrl,
'home.bundle'
)
console.log('Bundle 下载完成:', destPath)
// 缓存至本地,由 BundleRuntime 加载
}
```
> 与第 7 节「检查更新」中的示例相比,此处展示了 `UpdateSDK` 的完整独立 API。HarmonyOS 的整包更新只提供应用市场跳转,不提供本地安装包下载。
配置私有仓库(`.ohpmrc` 配置私有仓库(`.ohpmrc`
``` ```
@ -168,3 +232,67 @@ struct ChatView {
] ]
} }
``` ```
---
## 8. Push 接入
HarmonyOS SDK 提供 `PushSDK` 用于注册和注销推送 Token。业务层需在获取到 HarmonyOS 推送服务的 Token 后调用注册接口。
```typescript
import { PushSDK } from '@xuqm/harmony-sdk'
// 注册 Push Token在获取到系统推送 Token 后调用)
await PushSDK.registerToken('harmony_push_token', 'user_001')
// 登出时注销 Token
await PushSDK.unregisterToken('harmony_push_token')
```
> HarmonyOS 推送 Token 通过系统 Push Kit 获取,具体接入请参考 HarmonyOS 官方推送文档。SDK 内部 `vendor` 固定为 `HARMONY`,`platform` 固定为 `harmony`
---
## 9. 版本更新
HarmonyOS SDK 提供整包检查和 RN Bundle 热更新能力。
### 9.1 检查 App 更新
```typescript
import { UpdateSDK } from '@xuqm/harmony-sdk'
const result = await UpdateSDK.checkAppUpdate('your_app_key')
if (result.hasUpdate) {
console.log('发现新版本:', result.info?.latestVersionCode)
// HarmonyOS 整包更新只提供应用市场跳转
if (result.info?.marketUrl) {
await UpdateSDK.openAppMarket(getContext(this), result.info.marketUrl)
}
}
```
### 9.2 RN Bundle 热更新
```typescript
const rnResult = await UpdateSDK.checkRnUpdate(
'your_app_key',
'home', // bundle 名称
1 // 当前 bundle 版本号
)
if (rnResult.hasUpdate) {
console.log('发现 RN Bundle 更新:', rnResult.info?.bundleVersion)
// 下载 Bundle
const destPath = await UpdateSDK.downloadRnBundle(
getContext(this),
rnResult.info!.downloadUrl,
'home.bundle'
)
console.log('Bundle 下载完成:', destPath)
// 缓存至本地,由 BundleRuntime 加载
}
```
> 与第 7 节「检查更新」中的示例相比,此处展示了 `UpdateSDK` 的完整独立 API。HarmonyOS 的整包更新只提供应用市场跳转,不提供本地安装包下载。

查看文件

@ -0,0 +1,106 @@
# HarmonyOS 安装配置
**包名**`@xuqm/harmony-sdk` · **版本**0.1.0 · **语言**ArkTS
---
## ohpm 安装
在 HarmonyOS 项目的 `oh-package.json5` 中添加:
```json5
{
"dependencies": {
"@xuqm/harmony-sdk": "^0.1.0"
}
}
```
然后执行:
```bash
ohpm install
```
> 发布仓库:`https://ohpm.openharmony.cn/ohpm/`
---
## 最低 API 版本
- API 12+HarmonyOS 4.0+
- DevEco Studio 4.0 Release 或更高版本
---
## 权限配置
`module.json5` 中声明所需权限:
```json5
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
},
{
"name": "ohos.permission.GET_NETWORK_INFO"
}
]
}
}
```
如需使用相机、相册、麦克风等功能,额外添加:
```json5
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.CAMERA"
},
{
"name": "ohos.permission.READ_MEDIA"
},
{
"name": "ohos.permission.WRITE_MEDIA"
},
{
"name": "ohos.permission.MICROPHONE"
}
]
}
}
```
---
## 初始化
```ts
import { XuqmSDK } from '@xuqm/harmony-sdk'
import common from '@ohos.app.ability.common'
// 在 EntryAbility.onCreate 或页面初始化时
const context = getContext(this) as common.UIAbilityContext
await XuqmSDK.init(context, {
appKey: 'your_app_key',
appSecret: 'your_app_secret',
})
```
---
## 登录
```ts
const session = await XuqmSDK.login('user_001', 'your_user_sig_jwt')
```
---
## 下一步
- [HarmonyOS IM 接入 →](./im)

254
docs-site/docs/ios/im.md 普通文件
查看文件

@ -0,0 +1,254 @@
# iOS IM 接入
基于 `XuqmSDK` 中的 IM 子模块实现即时通讯功能。
---
## 初始化与登录
### 初始化
```swift
import XuqmSDK
let config = SDKConfig(appKey: "your_app_key", appSecret: "your_app_secret")
XuqmSDK.shared.initialize(config: config)
```
### 登录UserSig 模式)
```swift
import XuqmSDK
try await XuqmSDK.shared.login(userId: "user_001", userSig: "your_user_sig_jwt")
```
> `userSig` 由业务服务端用 `appSecret` 签发。若需更新登录态,直接重新登录即可。
---
## 消息收发
### 设置事件代理
```swift
import XuqmSDK
ImSDK.shared.setDelegate(self)
extension ViewController: ImEventDelegate {
func imClientDidConnect() { print("WS connected") }
func imClientDidReceiveMessage(_ msg: ImMessage) { /* 处理单聊消息 */ }
func imClientDidReceiveGroupMessage(_ msg: ImMessage) { /* 处理群消息 */ }
func imClientDidReadMessage(_ msg: ImMessage) { /* 对方已读回执 */ }
func imClientDidReceiveRevokedMessage(_ msg: ImMessage) { /* 消息被撤回 */ }
func imClientDidDisconnect(reason: String?) { /* 断线 */ }
func imClientDidError(_ error: String) { /* 错误 */ }
}
```
### 发送文本消息
```swift
let msg = ImSDK.shared.sendTextMessage(
toId: "user_002",
chatType: .single,
content: "Hello!"
)
```
### 发送多媒体消息
```swift
// 图片
let msg = ImSDK.shared.sendImageMessage(
toId: "user_002",
chatType: .single,
url: "https://cdn.example.com/img.jpg",
width: 800,
height: 600
)
// 视频
let msg = ImSDK.shared.sendVideoMessage(
toId: "user_002",
chatType: .single,
url: "https://cdn.example.com/video.mp4",
duration: 15,
size: 5_000_000
)
// 语音
let msg = ImSDK.shared.sendAudioMessage(
toId: "user_002",
chatType: .single,
url: "https://cdn.example.com/audio.mp3",
duration: 5,
size: 100_000
)
// 文件
let msg = ImSDK.shared.sendFileMessage(
toId: "user_002",
chatType: .single,
url: "https://cdn.example.com/file.pdf",
name: "document.pdf",
size: 2_000_000
)
// 位置
let msg = ImSDK.shared.sendLocationMessage(
toId: "user_002",
chatType: .single,
lat: 31.2304,
lng: 121.4737,
title: "上海",
address: "上海市黄浦区"
)
```
### 撤回与编辑
```swift
try await ImSDK.shared.revokeMessage(messageId: msg.id)
try await ImSDK.shared.editMessage(messageId: msg.id, content: "新内容")
```
### 获取历史消息
```swift
let history = try await ImSDK.shared.fetchHistory(toId: "user_002", page: 0, size: 20)
let groupHistory = try await ImSDK.shared.fetchGroupHistory(groupId: "group_xxx", page: 0, size: 20)
```
---
## 群聊
### 创建群组
```swift
let group = try await ImSDK.shared.createGroup(
name: "项目讨论",
memberIds: ["user_001", "user_002"],
groupType: "WORK"
)
```
### 订阅群消息
```swift
ImSDK.shared.subscribeGroup(group.id)
```
### 发送群消息
```swift
ImSDK.shared.sendTextMessage(toId: group.id, chatType: .group, content: "大家好")
```
### 群管理
```swift
// 添加/移除成员
try await ImSDK.shared.addGroupMember(groupId: group.id, userId: "user_003")
try await ImSDK.shared.removeGroupMember(groupId: group.id, targetUserId: "user_003")
// 设置角色
try await ImSDK.shared.setGroupRole(groupId: group.id, userId: "user_003", role: "ADMIN")
// 禁言
try await ImSDK.shared.muteGroupMember(groupId: group.id, userId: "user_003", minutes: 60)
// 转让群主
try await ImSDK.shared.transferGroupOwner(groupId: group.id, newOwnerId: "user_002")
// 退出/解散
try await ImSDK.shared.leaveGroup(groupId: group.id)
try await ImSDK.shared.dismissGroup(groupId: group.id)
```
---
## 好友管理
```swift
let friends = try await ImSDK.shared.listFriends()
try await ImSDK.shared.addFriend(friendId: "user_002")
try await ImSDK.shared.removeFriend(friendId: "user_002")
// 好友请求
try await ImSDK.shared.sendFriendRequest(toUserId: "user_002", remark: "你好")
let requests = try await ImSDK.shared.listFriendRequests(direction: "incoming")
try await ImSDK.shared.acceptFriendRequest(requestId: "request_id")
try await ImSDK.shared.rejectFriendRequest(requestId: "request_id")
// 黑名单
let blacklist = try await ImSDK.shared.listBlacklist()
try await ImSDK.shared.addToBlacklist(blockedUserId: "user_002")
try await ImSDK.shared.removeFromBlacklist(blockedUserId: "user_002")
```
---
## 会话列表
```swift
let conversations = try await ImSDK.shared.listConversations()
try await ImSDK.shared.setConversationPinned(targetId: "user_002", chatType: .single, pinned: true)
try await ImSDK.shared.setConversationMuted(targetId: "user_002", chatType: .single, muted: true)
try await ImSDK.shared.markRead(targetId: "user_002", chatType: .single)
try await ImSDK.shared.setDraft(targetId: "user_002", chatType: .single, draft: "未完成")
try await ImSDK.shared.deleteConversation(targetId: "user_002", chatType: .single)
```
---
## 离线消息同步
```swift
let count = try await ImSDK.shared.offlineMessageCount()
let offlineMessages = try await ImSDK.shared.syncOfflineMessages()
```
---
## 连接状态监听
```swift
let state = ImSDK.shared.connectionState
// .disconnected / .connecting / .connected
ImSDK.shared.addConnectionStateListener { state in
switch state {
case .connected: print("IM 已连接")
case .connecting: print("IM 连接中...")
case .disconnected: print("IM 已断开")
}
}
```
---
## 消息类型
| MsgType | 说明 | content 结构 |
|---------|------|-------------|
| `.text` | 纯文本 | `String` |
| `.image` | 图片 | `{url, width, height, thumbnailUrl?}` |
| `.video` | 视频 | `{url, duration, thumbnailUrl, size}` |
| `.audio` | 语音 | `{url, duration, size}` |
| `.file` | 文件 | `{url, name, size, mimeType}` |
| `.location` | 位置 | `{lat, lng, address, title}` |
| `.custom` | 自定义 | 任意 JSON |
| `.notify` | 系统通知 | `{title, content}` |
| `.richText` | 富文本 | `{html}` |
| `.callAudio` | 语音通话信令 | `{action}` |
| `.callVideo` | 视频通话信令 | `{action}` |
| `.forward` | 转发 | `{originalSender, originalContent}` |
| `.quote` | 引用 | `{quotedMsgId, quotedContent, text}` |
| `.merge` | 合并转发 | `{title, msgList}` |
| `.revoked` | 撤回 | 系统内部填充 |
[→ 完整 API Reference](./api)

142
docs-site/docs/ios/push.md 普通文件
查看文件

@ -0,0 +1,142 @@
# iOS 推送接入指南
**模块**`XuqmPush` · **支持**APNs默认、FCM可选
---
## 1. 添加依赖
`Package.swift` 中按需引入:
```swift
.target(
name: "MyApp",
dependencies: [
.product(name: "XuqmPush", package: "XuqmGroup-iOSSDK"),
]
)
```
如需 FCM 支持,额外在 Xcode 中集成 `FirebaseMessaging`
---
## 2. 申请通知权限
在应用启动时请求用户授权:
```swift
import XuqmPush
let granted = try await PushSDK.shared.requestAuthorization(options: [.alert, .badge, .sound])
if granted {
print("用户已授权通知")
}
```
`requestAuthorization` 内部会自动调用 `UIApplication.shared.registerForRemoteNotifications()`,无需额外处理。
---
## 3. 注册 APNs Token
`AppDelegate` 中转发系统回调:
```swift
import XuqmSDK
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
// 将 deviceToken 转发生给 SDK
XuqmSDK.shared.registerDeviceToken(deviceToken)
}
func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
print("注册远程通知失败: \(error.localizedDescription)")
}
```
> `XuqmSDK.shared.login()` 成功后会自动将 APNs Token 上报到 Push 服务端,业务层无需手动调用 `PushSDK.shared.registerDeviceToken()`
---
## 4. FCM 支持(可选)
如需集成 Firebase Cloud Messaging
1. 在 Xcode 中添加 `FirebaseMessaging` 依赖
2. 配置 `GoogleService-Info.plist`
3. 在获取到 FCM Token 后手动注册:
```swift
import XuqmPush
// 在 Firebase Messaging 回调中获取 FCM Token
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
guard let token = fcmToken else { return }
try await PushSDK.shared.registerFcmToken(token, userId: "user_001")
}
```
可通过 `PushSDK.shared.isFcmAvailable` 检查当前是否编译了 FCM
```swift
if PushSDK.shared.isFcmAvailable {
print("FCM 模块已集成")
}
```
---
## 5. 接收推送消息
实现 `PushMessageDelegate` 处理推送消息:
```swift
import XuqmPush
class MyPushHandler: NSObject, PushMessageDelegate {
func pushSDK(_ sdk: PushSDK, didReceiveMessage message: PushMessage) {
// 应用在前台时收到通知
print("收到推送: \(message.title ?? "") - \(message.body ?? "")")
print("Payload: \(message.payload)")
}
func pushSDK(_ sdk: PushSDK, didTapNotification message: PushMessage) {
// 用户点击通知栏消息
print("用户点击了通知: \(message.title ?? "")")
// 可在此跳转至对应会话页面
}
}
// 设置代理
PushSDK.shared.delegate = MyPushHandler()
```
`PushSDK` 内部已实现 `UNUserNotificationCenterDelegate`,会自动处理前台展示和点击事件。业务层只需设置 `delegate` 即可接收回调。
---
## 6. 多模块统一登录
Push 模块与 IM、Update 模块共享同一套登录态:
```swift
// 登录成功后自动触发
try await XuqmSDK.shared.login(userId: "user_001", userSig: userSig)
// ↓ 自动触发
// · PushSDK 注册 APNs Token 并上报
```
---
## 7. 登出时注销 Token
```swift
try await PushSDK.shared.unregisterToken(userId: "user_001")
```

94
docs-site/docs/ios/setup.md 普通文件
查看文件

@ -0,0 +1,94 @@
# iOS 安装配置
**版本**0.1.0 · **最低 iOS 版本**iOS 16 · **语言**Swift 5.9+
---
## Swift Package Manager推荐
在 Xcode → File → Add Package Dependencies 中输入:
```
https://github.com/xuqm/XuqmGroup-iOSSDK
```
或在 `Package.swift` 中添加:
```swift
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "MyApp",
platforms: [.iOS(.v16)],
dependencies: [
.package(url: "https://github.com/xuqm/XuqmGroup-iOSSDK", from: "0.1.0")
],
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "XuqmSDK", package: "XuqmGroup-iOSSDK")
]
)
]
)
```
> 目前 iOS SDK 以单模块 `XuqmSDK` 发布,内部包含 Core、IM、Push、Update 子模块。
---
## 最低 iOS 版本
- iOS 16.0+
- macOS 13.0+(如需 Mac Catalyst
- Xcode 16.0+
---
## 权限声明
根据业务需要,在 `Info.plist` 中添加以下权限:
### Camera拍照/视频通话)
```xml
<key>NSCameraUsageDescription</key>
<string>需要访问相机以发送图片或进行视频通话</string>
```
### Photo Library发送图片/视频)
```xml
<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问相册以发送图片和视频</string>
```
### Microphone语音消息/语音通话)
```xml
<key>NSMicrophoneUsageDescription</key>
<string>需要访问麦克风以发送语音消息或进行通话</string>
```
---
## 初始化
`AppDelegate``SceneDelegate` 中:
```swift
import XuqmSDK
let config = SDKConfig(appKey: "your_app_key", appSecret: "your_app_secret")
XuqmSDK.shared.initialize(config: config)
```
---
## 下一步
- [iOS IM 接入 →](./im)
- [iOS Push 接入 →](./push)
- [iOS 版本更新 →](./update)

查看文件

@ -0,0 +1,93 @@
# iOS 版本更新接入指南
**模块**`XuqmUpdate` · **功能**App 版本检查、App Store 跳转
---
## 1. 添加依赖
`Package.swift` 中按需引入:
```swift
.target(
name: "MyApp",
dependencies: [
.product(name: "XuqmUpdate", package: "XuqmGroup-iOSSDK"),
]
)
```
---
## 2. 检查更新
```swift
import XuqmUpdate
let appInfo = try await UpdateSDK.shared.checkAppUpdate(currentVersionCode: 1)
if let info = appInfo, info.needsUpdate {
print("发现新版本: \(info.versionName ?? "")")
print("更新日志: \(info.changeLog ?? "")")
if info.forceUpdate == true {
// 强制更新
showForceUpdateAlert(info)
} else {
// 可选更新
showOptionalUpdateAlert(info)
}
}
```
---
## 3. 跳转 App Store
```swift
if let url = appInfo?.appStoreUrl {
UpdateSDK.shared.openAppStore(url: url)
}
```
`openAppStore` 会调用 `UIApplication.shared.open()` 打开 App Store 页面,引导用户前往商店更新。
---
## 4. 强制更新处理
当服务端返回 `forceUpdate = true` 时,建议业务层:
1. 弹出 `UIAlertController`,设置 `isUserInteractionEnabled = false` 禁止关闭
2. 只允许用户点击「前往更新」
3. 调用 `UpdateSDK.shared.openAppStore(url:)` 跳转 App Store
```swift
func showForceUpdateAlert(_ info: AppUpdateInfo) {
let alert = UIAlertController(
title: "发现重要更新",
message: "当前版本已不可用,请升级至 \(info.versionName ?? "最新版")",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "前往更新", style: .default) { _ in
if let url = info.appStoreUrl ?? info.downloadUrl {
UpdateSDK.shared.openAppStore(url: url)
}
})
present(alert, animated: true)
}
```
---
## 5. 多模块统一登录
Update 模块与 IM、Push 模块共享同一套登录态:
```swift
// 初始化
XuqmSDK.shared.initialize(config: config)
// 登录(业务登录成功后调用一次)
try await XuqmSDK.shared.login(userId: "user_001", userSig: userSig)
// UpdateSDK 在 checkAppUpdate 时自动携带 appId,无需额外登录操作
```

查看文件

@ -0,0 +1,206 @@
# 微信小程序 SDK 概览
**包名**`@xuqm/miniprogram-sdk` · **版本**0.1.0
---
## npm 安装
```bash
npm install @xuqm/miniprogram-sdk
```
或在微信开发者工具中:
1. 打开「工具」→「构建 npm」
2. 在小程序 `package.json` 中添加上述依赖
---
## 初始化
```ts
import { XuqmMiniProgramSDK } from '@xuqm/miniprogram-sdk'
const sdk = new XuqmMiniProgramSDK()
sdk.init({
appKey: 'your_app_key',
appSecret: 'your_app_secret',
debug: true, // 可选
})
```
---
## IM 接入
### 登录
```ts
// 使用 UserSig 登录
await sdk.login('user_001', 'your_user_sig_jwt')
// 或使用演示账号快速登录(仅测试)
const token = await sdk.loginWithDemo('user_001', '123456')
```
### 监听消息
```ts
sdk.on('connected', () => {
console.log('IM 已连接')
})
sdk.on('message', (msg) => {
console.log('收到消息:', msg.msgType, msg.content)
})
sdk.on('read', (msg) => {
console.log('已读回执:', msg.id)
})
sdk.on('revoke', (data) => {
console.log('消息被撤回:', data.msgId)
})
sdk.on('disconnected', (reason) => {
console.log('断开连接:', reason)
})
sdk.on('error', (error) => {
console.error('IM 错误:', error)
})
```
### 发送消息
```ts
const msg = await sdk.send({
toId: 'user_002',
chatType: 'SINGLE',
msgType: 'TEXT',
content: 'Hello!',
})
```
### 发送文本消息(快捷方法)
```ts
await sdk.sendTextMessage('user_002', 'SINGLE', 'Hello!')
```
### 历史消息
```ts
const history = await sdk.fetchHistory('user_002')
const groupHistory = await sdk.fetchGroupHistory('group_xxx')
```
### 会话列表
```ts
const conversations = await sdk.listConversations()
await sdk.markRead('user_002')
await sdk.setConversationPinned('user_002', 'SINGLE', true)
await sdk.setConversationMuted('user_002', 'SINGLE', true)
```
### 群聊
```ts
const groups = await sdk.listGroups()
const group = await sdk.getGroupInfo('group_xxx')
const members = await sdk.listGroupMembers('group_xxx')
```
### 好友管理
```ts
const friends = await sdk.listFriends()
await sdk.addFriend('user_002')
await sdk.removeFriend('user_002')
```
### 离线消息同步
```ts
const count = await sdk.offlineMessageCount()
const messages = await sdk.syncOfflineMessages()
```
---
## Push 接入(小程序通知)
微信小程序使用**订阅消息**和**服务通知**实现 Push,由服务端调用微信 API 下发。
XuqmGroup 小程序 SDK 本身不直接处理 Push Token小程序无设备 Token 概念),业务方需要:
1. 用户订阅消息模板
2. 业务服务端调用 XuqmGroup Server SDK 的 `send_push` 接口
3. 或业务服务端直接调用微信服务端 API 下发订阅消息
```python
# Python Server SDK 示例
from xuqm_im_server_sdk import XuqmImServerSdk
sdk = XuqmImServerSdk(config)
sdk.send_push(
user_id="user_001",
title="新消息",
body="你有一条未读消息",
)
```
---
## Update 接入(小程序自带更新机制)
微信小程序的更新由微信客户端自动管理,开发者可通过微信 API 检查更新:
```ts
const updateManager = wx.getUpdateManager()
updateManager.onCheckForUpdate((res) => {
console.log('是否有新版本:', res.hasUpdate)
})
updateManager.onUpdateReady(() => {
wx.showModal({
title: '更新提示',
content: '新版本已准备好,是否重启应用?',
success: (res) => {
if (res.confirm) {
updateManager.applyUpdate()
}
},
})
})
```
> XuqmGroup 小程序 SDK 不额外提供 Update 模块,直接使用微信小程序原生更新能力即可。
---
## 消息类型
| MsgType | 说明 |
|---------|------|
| `TEXT` | 纯文本 |
| `IMAGE` | 图片 |
| `VIDEO` | 视频 |
| `AUDIO` | 语音 |
| `FILE` | 文件 |
| `LOCATION` | 位置 |
| `CUSTOM` | 自定义 |
| `NOTIFY` | 系统通知 |
| `RICH_TEXT` | 富文本 |
| `CALL_AUDIO` | 语音通话信令 |
| `CALL_VIDEO` | 视频通话信令 |
| `QUOTE` | 引用 |
| `MERGE` | 合并转发 |
| `FORWARD` | 转发 |
| `REVOKED` | 撤回 |
[→ Server API 文档 →](/server/api)

109
docs-site/docs/rn/group.md 普通文件
查看文件

@ -0,0 +1,109 @@
# React Native 群聊
基于 `@xuqm/rn-im` 模块实现群组相关功能。
---
## 创建群聊
```ts
import { ImSDK } from '@xuqm/rn-im'
const group = await ImSDK.createGroup('项目讨论', ['user_002', 'user_003'])
// group.id — 群 ID
// group.name — 群名称
// group.creatorId — 创建者
```
> `createGroup` 第二个参数为初始成员列表,创建者自动加入。
---
## 邀请成员
```ts
// 添加单个成员
await ImSDK.addGroupMember('group_xxx', 'user_004')
// 批量添加成员
await ImSDK.batchAddGroupMembers('group_xxx', ['user_004', 'user_005'])
```
---
## 发送群消息
```ts
const msg = await ImSDK.sendMessage(
'group_xxx', // toId
'GROUP', // chatType
'TEXT', // msgType
'大家好!' // content
)
```
发送多媒体群消息:
```ts
// 图片
await ImSDK.sendImageMessage('group_xxx', 'GROUP', '/path/to/image.jpg', 800, 600)
```
---
## 群成员管理
```ts
// 移除成员
await ImSDK.removeGroupMember('group_xxx', 'user_004')
// 批量移除
await ImSDK.batchRemoveGroupMembers('group_xxx', ['user_004', 'user_005'])
// 退出群聊
await ImSDK.leaveGroup('group_xxx')
// 设置管理员角色(示例)
await ImSDK.setGroupRole('group_xxx', 'user_004', 'ADMIN')
// 禁言成员(示例)
await ImSDK.muteGroupMember('group_xxx', 'user_004', 60)
// 转让群主(示例)
await ImSDK.transferGroupOwner('group_xxx', 'user_002')
// 解散群聊(示例)
await ImSDK.dismissGroup('group_xxx')
```
---
## 群信息查询
```ts
// 群列表(仅返回当前用户所在的群)
const groups = await ImSDK.listGroups()
// 群详情
const group = await ImSDK.getGroupInfo('group_xxx')
// 群成员
const members = await ImSDK.listGroupMembers('group_xxx')
// 群历史消息
const history = await ImSDK.fetchGroupHistory('group_xxx', page, size)
```
---
## 群类型
创建群时可指定 `groupType`
| 类型 | 说明 |
|------|------|
| `WORK` | 工作群(默认)|
| `PUBLIC` | 公开群 |
| `PRIVATE` | 私有群 |
[→ 返回 RN IM 接入文档](./im)

201
docs-site/docs/rn/im.md 普通文件
查看文件

@ -0,0 +1,201 @@
# React Native IM 接入
基于 `@xuqm/rn-im` 模块实现即时通讯功能,使用 WatermelonDB 进行本地消息存储。
---
## 初始化与登录
### 初始化
```ts
import { XuqmSDK } from '@xuqm/rn-common'
await XuqmSDK.initialize({
appKey: 'your_app_key',
logLevel: __DEV__ ? 'debug' : 'warn',
})
```
### 登录
```ts
import { XuqmSDK } from '@xuqm/rn-common'
await XuqmSDK.login({
userId: 'user_001',
userSig: 'your_user_sig_jwt',
})
```
> 如果集成了 `rn-im`,`XuqmSDK.login` 会自动触发 `ImSDK` 连接 WebSocket,业务侧无需单独调用 `ImSDK.login`
---
## 消息收发
### 监听实时消息
```ts
import { ImSDK } from '@xuqm/rn-im'
ImSDK.addEventListener('message', (msg) => {
console.log('收到消息:', msg.msgType, msg.content)
})
ImSDK.addEventListener('read', (msg) => {
console.log('已读回执:', msg.id)
})
ImSDK.addEventListener('revoke', (data) => {
console.log('消息被撤回:', data.msgId)
})
```
### 发送文本消息
```ts
const msg = await ImSDK.sendMessage(
'user_002', // toId
'SINGLE', // chatType: 'SINGLE' | 'GROUP'
'TEXT', // msgType
'Hello!' // content
)
```
### 发送图片
```ts
const msg = await ImSDK.sendImageMessage(
'user_002',
'SINGLE',
'/path/to/image.jpg', // 本地 URI
800, // 宽
600 // 高
)
```
### 获取历史消息
```ts
// 单聊历史
const history = await ImSDK.fetchHistory('user_002', page, size)
// 群历史
const groupHistory = await ImSDK.fetchGroupHistory('group_xxx', page, size)
```
---
## WatermelonDB 本地存储
`rn-im` 使用 WatermelonDBSQLite存储本地消息,按 `appKey + userId` 自动隔离。
```ts
import { ImDatabase } from '@xuqm/rn-im'
// 消息搜索(本地)
const params: MessageSearchParams = {
keyword: '会议',
toId: 'user_002',
chatType: 'SINGLE',
msgTypes: ['TEXT'],
limit: 20,
}
const results = await ImSDK.searchMessages(params)
```
---
## 群聊
```ts
// 创建群
const group = await ImSDK.createGroup('项目讨论', ['user_002', 'user_003'])
// 群列表
const groups = await ImSDK.listGroups()
// 添加成员
await ImSDK.addGroupMember('group_xxx', 'user_004')
// 移除成员
await ImSDK.removeGroupMember('group_xxx', 'user_004')
// 退出群聊
await ImSDK.leaveGroup('group_xxx')
```
详见 [RN 群聊文档 →](./group)
---
## 会话列表
```ts
// 订阅会话变化
const unsub = ImSDK.subscribeConversations((conversations) => {
console.log(conversations)
})
// 置顶
await ImSDK.setConversationPinned('user_002', 'SINGLE', true)
// 免打扰
await ImSDK.setConversationMuted('group_xxx', 'GROUP', true)
// 标记已读
await ImSDK.markRead('user_002')
```
---
## 离线消息同步
```ts
// 查询离线消息数量(示例)
const count = await ImSDK.offlineMessageCount()
// 同步离线消息(示例)
const messages = await ImSDK.syncOfflineMessages()
```
---
## 断开连接
```ts
ImSDK.disconnect()
```
---
## 类型参考
```ts
interface ImMessage {
id: string
fromId: string
toId: string
chatType: 'SINGLE' | 'GROUP'
msgType: 'TEXT' | 'IMAGE' | 'AUDIO' | 'VIDEO' | 'FILE' |
'LOCATION' | 'NOTIFY' | 'CUSTOM' | 'RICH_TEXT' |
'CALL_AUDIO' | 'CALL_VIDEO' | 'FORWARD' | 'REVOKED'
content: string
status: 'SENDING' | 'SENT' | 'DELIVERED' | 'READ' | 'FAILED' | 'REVOKED'
createdAt: number
}
interface ConversationData {
targetId: string
chatType: 'SINGLE' | 'GROUP'
lastMsgContent: string
lastMsgType: string
lastMsgTime: number
unreadCount: number
isMuted: boolean
isPinned: boolean
}
```
[→ 完整 API Reference](./api)

108
docs-site/docs/rn/setup.md 普通文件
查看文件

@ -0,0 +1,108 @@
# React Native 安装配置
**包名**`@xuqm/rn-sdk` · **版本**0.2.x · **RN 版本**:≥ 0.76.0
> `rn-sdk` 作为内部基础包存在,业务方正常接入时使用 `@xuqm/rn-common` 和各业务模块即可。
---
## npm / yarn 安装
在项目根目录创建 `.npmrc`
```
@xuqm:registry=https://nexus.xuqinmin.com/repository/npm-hosted/
```
只使用基础能力时,直接安装 `rn-common`
```bash
yarn add @xuqm/rn-common
```
按需安装模块时,`rn-im` / `rn-push` / `rn-update` 都会自动带上 `rn-common``rn-sdk`
```bash
yarn add @xuqm/rn-common @xuqm/rn-im
# 或全量安装
yarn add @xuqm/rn-common @xuqm/rn-im @xuqm/rn-push @xuqm/rn-update
```
---
## 自动 / 手动链接
React Native 0.60+ 支持**自动链接**,安装后执行:
```bash
cd ios && pod install
```
> 若使用 Expo,需先执行 `expo prebuild` 生成原生项目后再执行 `pod install`
---
## iOS 配置
### Pod 安装
```bash
cd ios
pod install
```
### 权限声明
`Info.plist` 中添加:
```xml
<key>NSCameraUsageDescription</key>
<string>需要访问相机</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问相册</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要访问麦克风</string>
```
---
## Android 配置
### Gradle 仓库
`android/build.gradle` 中确保包含:
```gradle
allprojects {
repositories {
maven { url "https://nexus.xuqinmin.com/repository/android/" }
google()
mavenCentral()
}
}
```
### 最低版本
- `minSdkVersion = 24`
- `compileSdkVersion = 34`
---
## 依赖关系
```
@xuqm/rn-sdkmeta-package,不建议业务方直接引用
├── @xuqm/rn-common ← 初始化、网络、设备信息
├── @xuqm/rn-im ← IM 模块(依赖 WatermelonDB
├── @xuqm/rn-push ← Push 模块
└── @xuqm/rn-update ← 更新模块
```
---
## 下一步
- [RN IM 接入 →](./im)
- [RN 群聊 →](./group)
- [RN 版本更新 →](./update)

154
docs-site/docs/rn/update.md 普通文件
查看文件

@ -0,0 +1,154 @@
# React Native 版本更新接入指南
**包名**`@xuqm/rn-update` · **功能**App 版本检查、RN Bundle 热更新、打开商店
---
## 1. 安装
```bash
yarn add @xuqm/rn-update
```
`rn-update` 会自动依赖 `@xuqm/rn-common``@xuqm/rn-sdk`
---
## 2. App 版本检查
```ts
import { UpdateSDK } from '@xuqm/rn-update'
const appUpdate = await UpdateSDK.checkAppUpdate()
if (appUpdate.needsUpdate) {
console.log('新版本:', appUpdate.versionName)
console.log('更新日志:', appUpdate.changeLog)
console.log('下载地址:', appUpdate.downloadUrl)
if (appUpdate.forceUpdate) {
// 强制更新
showForceUpdateModal(appUpdate)
} else {
// 可选更新
showOptionalUpdateModal(appUpdate)
}
}
```
App 版本号自动从原生模块读取,无需手动传入。开发/模拟器环境可调用 `_devSetAppVersion` 覆盖:
```ts
// 仅限开发环境使用
UpdateSDK._devSetAppVersion(100, '1.0.0')
```
---
## 3. 打开商店
```ts
await UpdateSDK.openStore(appUpdate.appStoreUrl, appUpdate.marketUrl)
```
- iOS打开 `appStoreUrl`
- Android打开 `marketUrl`
---
## 4. RN Bundle 热更新
### 4.1 注册插件
在插件 Bundle 的入口文件顶部注册插件元数据:
```ts
// plugin/index.ts
import { UpdateSDK } from '@xuqm/rn-update'
import meta from './plugin.json'
UpdateSDK.registerPlugin(meta)
```
`plugin.json` 示例:
```json
{
"moduleId": "home",
"version": "1.2.3"
}
```
### 4.2 检查热更新
```ts
const rnUpdate = await UpdateSDK.checkRnUpdate('home')
if (rnUpdate.needsUpdate) {
console.log('最新版本:', rnUpdate.latestVersion)
console.log('下载地址:', rnUpdate.downloadUrl)
console.log('更新说明:', rnUpdate.note)
console.log('最低依赖版本:', rnUpdate.minCommonVersion)
}
```
### 4.3 下载并缓存 Bundle
```ts
// 下载 Bundle 源码
const source = await UpdateSDK.downloadRnBundle(rnUpdate.downloadUrl)
// 缓存到本地
await UpdateSDK.cacheRnBundle('home', rnUpdate.latestVersion, rnUpdate.md5, source)
// 读取已缓存的 Bundle
const cached = await UpdateSDK.getCachedRnBundle('home')
if (cached) {
console.log('缓存版本:', cached.version)
console.log('缓存时间:', cached.downloadedAt)
}
```
### 4.4 获取已注册插件版本
```ts
const version = UpdateSDK.getRegisteredPluginVersion('home')
console.log('当前运行版本:', version)
```
---
## 5. 强制更新处理
`appUpdate.forceUpdate``true` 时,建议业务层:
1. 弹出不可关闭的 Modal
2. 只允许用户点击「立即更新」
3. 调用 `UpdateSDK.openStore()` 跳转商店
```ts
function showForceUpdateModal(update: AppUpdateInfo) {
// 使用 RN Modal 组件,设置不可取消
Alert.alert(
'发现重要更新',
`当前版本已不可用,请升级至 ${update.versionName}`,
[
{
text: '立即更新',
onPress: () => UpdateSDK.openStore(update.appStoreUrl, update.marketUrl),
},
],
{ cancelable: false }
)
}
```
---
## 6. 多模块统一登录
Update 模块与 IM、Push 模块共享同一套登录态:
```ts
await XuqmSDK.initialize({ appKey: 'your_app_key' })
await XuqmSDK.login({ userId: 'user_001', userSig: 'your_user_sig_jwt' })
// UpdateSDK 在 checkAppUpdate 时自动携带 appId,无需额外登录操作
```

查看文件

@ -0,0 +1,105 @@
# 错误码文档
---
## HTTP 错误码
| 状态码 | 说明 | 常见场景 |
|--------|------|----------|
| 200 | 成功 | 请求正常处理 |
| 400 | 请求参数错误 | 缺少必填字段、参数类型不匹配 |
| 401 | 未授权 | Token 无效、过期、未携带 |
| 403 | 禁止访问 | 权限不足、黑名单限制 |
| 404 | 资源不存在 | 用户不存在、群不存在、消息不存在 |
| 500 | 服务端内部错误 | 异常未捕获 |
所有接口统一返回格式:
```json
{
"code": 200,
"status": "OK",
"data": { ... },
"message": ""
}
```
业务错误时:
```json
{
"code": 400,
"status": "BAD_REQUEST",
"data": null,
"message": "参数错误userId 不能为空"
}
```
---
## WebSocket 错误码
| 关闭码 | 说明 | 处理建议 |
|--------|------|----------|
| 1000 | 正常关闭 | 无需重连(如用户主动登出)|
| 1001 | 服务端关闭 | 尝试重连 |
| 1006 | 异常断开 | 自动重连 |
| 1008 | 策略违反(如 Token 无效)| 重新获取 Token 后重连 |
| 1011 | 服务端异常 | 指数退避重连 |
---
## SDK 错误码
### Android
| 错误 | 说明 |
|------|------|
| `XuqmSDK not initialized. Call XuqmSDK.initialize() first.` | 未调用初始化 |
| `WebSocket not connected` | 发送消息时未连接 |
| `edit message failed` | 编辑消息接口返回空 |
| `revoke message failed` | 撤回消息接口返回空 |
### iOS
| 错误 | 说明 |
|------|------|
| `XuqmSDK not initialized. Call XuqmSDK.shared.initialize() first.` | 未调用初始化 |
### Web / Vue3 / 小程序
| 错误 | 说明 |
|------|------|
| `XuqmSDK not initialized. Call init() first.` | 未调用初始化 |
| `WebSocket not connected` | WebSocket 未连接时发送消息 |
| `IM token is empty` | 未登录或 Token 已清空 |
---
## 常见错误排查
### Q: 登录后 WebSocket 无法连接
- 检查 `userSig` 是否有效(未过期、签名正确)
- 检查网络是否允许 WebSocket防火墙、代理
- 检查 WebSocket 地址是否正确
### Q: 发送消息返回 FAILED
- 检查 WebSocket 连接状态
- 检查 `toId` 是否有效
- 检查 `appKey` 是否正确配置
### Q: 收不到消息
- 确认已订阅 `/user/queue/messages`
- 确认发送方和接收方使用同一 `appKey`
- 检查是否被对方拉黑
### Q: Token 401
- `userSig` 由服务端用 `appSecret` 签发,确保签名算法一致
- 检查 Token 是否在有效期内
- 检查 HTTP Header 中 `Authorization: Bearer {token}` 格式
[→ Server API 文档 →](./api)

查看文件

@ -0,0 +1,192 @@
# WebSocket 协议文档
XuqmGroup IM 使用 **STOMP over WebSocket** 协议进行实时消息通信。
---
## 连接地址
| 环境 | 地址 |
|------|------|
| 演示环境 | `wss://dev.xuqinmin.com/ws/im` |
| 生产环境 | 由租户平台分配 |
连接时需携带 `token` 参数:
```
wss://dev.xuqinmin.com/ws/im?token={userSig}
```
---
## 心跳机制
客户端在 STOMP CONNECT 帧中声明心跳:
```
CONNECT
accept-version:1.2
heart-beat:0,0
host:dev.xuqinmin.com
Authorization:Bearer {token}
\x00
```
> 当前服务端不强制心跳,客户端可依赖 WebSocket 原生 `onclose`/`onerror` 检测断线。
---
## 消息格式STOMP
### CONNECT 帧
客户端建立 WebSocket 连接后,发送 STOMP CONNECT
```
CONNECT
accept-version:1.2
heart-beat:0,0
host:dev.xuqinmin.com
Authorization:Bearer {token}
\x00
```
### CONNECTED 帧
服务端鉴权通过后返回:
```
CONNECTED
version:1.2
\x00
```
### SUBSCRIBE 帧
订阅个人消息队列(自动执行):
```
SUBSCRIBE
id:sub-1
destination:/user/queue/messages
\x00
```
订阅群消息:
```
SUBSCRIBE
id:sub-2
destination:/topic/group/{groupId}
\x00
```
### SEND 帧 — 发送消息
```
SEND
destination:/app/chat.send
content-type:application/json
{"appId":"ak_demo_chat","messageId":"...","toId":"user_002","chatType":"SINGLE","msgType":"TEXT","content":"Hello"}
\x00
```
### SEND 帧 — 撤回消息
```
SEND
destination:/app/chat.revoke
content-type:application/json
{"appId":"ak_demo_chat","messageId":"..."}
\x00
```
### SEND 帧 — 同步离线消息
```
SEND
destination:/app/chat.sync
content-type:application/json
{"appId":"ak_demo_chat"}
\x00
```
### MESSAGE 帧 — 接收消息
```
MESSAGE
destination:/user/queue/messages
message-id:...
{"id":"...","fromId":"user_002","toId":"user_001","chatType":"SINGLE","msgType":"TEXT","content":"Hello","status":"SENT","createdAt":1715000000000}
\x00
```
### ERROR 帧
```
ERROR
message:Unauthorized
Unauthorized
\x00
```
---
## 订阅路径
| 路径 | 说明 |
|------|------|
| `/user/queue/messages` | 个人消息队列(登录后自动订阅)|
| `/topic/group/{groupId}` | 群消息频道 |
---
## 服务端配置
基于 Spring WebSocket + STOMP
```java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
registry.setUserDestinationPrefix("/user");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws/im")
.setAllowedOriginPatterns("*")
.withSockJS();
registry.addEndpoint("/ws/im")
.setAllowedOriginPatterns("*");
}
}
```
---
## 自定义客户端接入
如果业务方不使用官方 SDK,可按以下流程自建客户端
1. 通过 `wss://host/ws/im?token=xxx` 建立 WebSocket
2. 发送 STOMP CONNECT 帧(携带 `Authorization: Bearer {token}`
3. 收到 CONNECTED 后,订阅 `/user/queue/messages`
4. 发送消息时构造 STOMP SEND 帧,`destination: /app/chat.send`
5. 收到服务端 MESSAGE 帧后解析 JSON body
[→ Server API 文档 →](./api)

226
docs-site/docs/vue3/im.md 普通文件
查看文件

@ -0,0 +1,226 @@
# Vue3 IM 接入
基于 `@xuqm/vue3-sdk` 实现即时通讯功能,支持原生 WebSocket + STOMP 协议。
---
## 初始化与登录
### 初始化
```ts
import { init } from '@xuqm/vue3-sdk'
init({
appKey: 'your_app_key',
appSecret: 'your_app_secret',
})
```
### 登录
```ts
import { login } from '@xuqm/vue3-sdk'
login('user_001', 'your_user_sig_jwt')
```
---
## 消息收发
### 使用 ImClient
```ts
import { ImClient } from '@xuqm/vue3-sdk'
const im = new ImClient()
im.on('connected', () => {
console.log('IM 已连接')
})
im.on('message', (msg) => {
console.log('收到消息:', msg.msgType, msg.content)
})
im.on('read', (msg) => {
console.log('已读回执:', msg.id)
})
im.on('revoke', (data) => {
console.log('消息被撤回:', data.msgId, data.operatorId)
})
im.on('disconnected', (code, reason) => {
console.log('断开连接:', code, reason)
})
im.on('error', (err) => {
console.error('IM 错误:', err)
})
im.connect()
```
### 发送消息
```ts
const outgoing = im.send({
toId: 'user_002',
chatType: 'SINGLE',
msgType: 'TEXT',
content: 'Hello!',
})
```
### 发送多媒体消息
```ts
// 图片
im.send({
toId: 'user_002',
chatType: 'SINGLE',
msgType: 'IMAGE',
content: JSON.stringify({ url: 'https://...', width: 800, height: 600 }),
})
// 位置
im.send({
toId: 'user_002',
chatType: 'SINGLE',
msgType: 'LOCATION',
content: JSON.stringify({ lat: 31.2304, lng: 121.4737, title: '上海' }),
})
```
### 撤回消息
```ts
im.revoke('message_id')
```
---
## 使用 Vue ComposableuseIm
```ts
import { useIm } from '@xuqm/vue3-sdk'
const im = useIm()
// 连接
im.connect()
// 发送消息
im.send({ toId: 'user_002', chatType: 'SINGLE', msgType: 'TEXT', content: 'Hello' })
// 响应式状态
console.log(im.connected.value) // boolean
console.log(im.messages.value) // ImMessage[]
console.log(im.conversations.value) // ConversationView[]
console.log(im.error.value) // Event | null
// 历史消息
const history = await im.loadHistory('user_002')
// 标记已读
await im.setConversationRead('user_002')
// 置顶/免打扰
await im.setConversationPinnedState('user_002', 'SINGLE', true)
await im.setConversationMutedState('user_002', 'SINGLE', true)
// 删除会话
await im.removeConversation('user_002', 'SINGLE')
// 断开
im.disconnect()
```
> `useIm` 在组件卸载时会自动调用 `disconnect()`
---
## 会话列表
```ts
import { listConversations } from '@xuqm/vue3-sdk'
const conversations = await listConversations()
```
会话数据类型:
```ts
interface ConversationView {
targetId: string
chatType: 'SINGLE' | 'GROUP'
lastMsgContent?: string | null
lastMsgType?: string | null
lastMsgTime: number
unreadCount: number
isMuted: boolean
isPinned: boolean
}
```
---
## 离线消息同步
```ts
import { offlineMessageCount, syncOfflineMessages } from '@xuqm/vue3-sdk'
const count = await offlineMessageCount()
const messages = await syncOfflineMessages()
```
---
## 群聊
```ts
import { createGroup, listGroups, addGroupMember, removeGroupMember } from '@xuqm/vue3-sdk'
const group = await createGroup('项目讨论', ['user_002', 'user_003'])
const groups = await listGroups()
await addGroupMember('group_xxx', 'user_004')
await removeGroupMember('group_xxx', 'user_004')
```
---
## 好友管理
```ts
import { listFriends, addFriend, removeFriend, sendFriendRequest } from '@xuqm/vue3-sdk'
const friends = await listFriends()
await addFriend('user_002')
await removeFriend('user_002')
```
---
## 消息类型
| MsgType | 说明 |
|---------|------|
| `TEXT` | 纯文本 |
| `IMAGE` | 图片 |
| `VIDEO` | 视频 |
| `AUDIO` | 语音 |
| `FILE` | 文件 |
| `LOCATION` | 位置 |
| `CUSTOM` | 自定义 |
| `NOTIFY` | 系统通知 |
| `RICH_TEXT` | 富文本 |
| `CALL_AUDIO` | 语音通话信令 |
| `CALL_VIDEO` | 视频通话信令 |
| `QUOTE` | 引用 |
| `MERGE` | 合并转发 |
| `FORWARD` | 转发 |
| `REVOKED` | 撤回 |
[→ 完整 API Reference](./api)

查看文件

@ -0,0 +1,78 @@
# Vue3 SDK 安装配置
**包名**`@xuqm/vue3-sdk` · **版本**0.1.0 · **Vue 版本**^3.5.0
---
## npm 安装
```bash
npm install @xuqm/vue3-sdk
# 或
yarn add @xuqm/vue3-sdk
# 或
pnpm add @xuqm/vue3-sdk
```
---
## CDN 方式
```html
<script type="module">
import { init, ImClient } from 'https://cdn.xuqinmin.com/npm/@xuqm/vue3-sdk@0.1.0/dist/index.es.js'
</script>
```
> CDN 地址为示例,实际以您的 CDN 部署为准。
---
## TypeScript 支持
`@xuqm/vue3-sdk` 内置完整类型定义,安装后无需额外配置 `@types` 包。
```ts
import type {
SDKConfig,
ImMessage,
ImGroup,
ChatType,
MsgType,
MsgStatus,
ConversationView,
PageResult,
UserProfile,
FriendRequest,
GroupJoinRequest,
BlacklistEntry,
BlacklistCheckResult,
GroupReadReceiptSummary,
HistoryQuery,
SendMessageParams,
ImEventMap,
ApiResponse,
} from '@xuqm/vue3-sdk'
```
---
## 初始化
```ts
import { init } from '@xuqm/vue3-sdk'
init({
appKey: 'your_app_key',
appSecret: 'your_app_secret',
debug: true, // 可选
baseUrl: 'https://...', // 可选,默认 https://dev.xuqinmin.com
wsUrl: 'wss://.../ws/im', // 可选,默认 wss://dev.xuqinmin.com/ws/im
})
```
---
## 下一步
- [Vue3 IM 接入 →](./im)

查看文件

@ -25,11 +25,18 @@ export interface TenantPage {
totalPages: number totalPages: number
} }
export interface DailyTrendItem {
date: string
count: number
}
export interface Statistics { export interface Statistics {
totalTenants: number totalTenants: number
todayNew: number todayNew: number
activeApps: number activeApps: number
onlineUsers: number onlineUsers: number
dailyTrend: DailyTrendItem[]
serviceDistribution: Record<string, number>
} }
export interface ServiceRequest { export interface ServiceRequest {

查看文件

@ -122,12 +122,25 @@ const ruleForm = reactive<RiskRuleForm>({
}) })
const ruleLoading = ref(false) const ruleLoading = ref(false)
async function loadRules() {
try {
const res = await opsApi.getRiskRules()
const cfg = res.data.data
if (cfg) {
ruleForm.ipRateLimit = cfg.ipRateLimit ?? 300
ruleForm.loginFailThreshold = cfg.loginFailThreshold ?? 5
ruleForm.loginLockMinutes = cfg.loginLockMinutes ?? 30
ruleForm.abnormalDetection = cfg.abnormalDetection ?? true
}
} catch {
// ignore
}
}
async function saveRules() { async function saveRules() {
ruleLoading.value = true ruleLoading.value = true
try { try {
// TODO: await opsApi.saveRiskRules({ ...ruleForm })
// await opsApi.saveRiskRules({ ...ruleForm })
await new Promise(r => setTimeout(r, 500))
ElMessage.success('配置已保存') ElMessage.success('配置已保存')
} catch { } catch {
ElMessage.error('保存失败') ElMessage.error('保存失败')
@ -171,11 +184,10 @@ function levelTagType(level: string) {
async function loadWords() { async function loadWords() {
tableLoading.value = true tableLoading.value = true
try { try {
// TODO: const res = await opsApi.listSensitiveWords(page.value - 1, size.value)
// const res = await opsApi.listSensitiveWords(page.value - 1, size.value) const data = res.data.data
await new Promise(r => setTimeout(r, 300)) wordList.value = data.content ?? []
wordList.value = generateMockWords(page.value, size.value) total.value = data.total ?? 0
total.value = 63
} catch { } catch {
ElMessage.error('加载失败') ElMessage.error('加载失败')
} finally { } finally {
@ -183,27 +195,6 @@ async function loadWords() {
} }
} }
function generateMockWords(pageNum: number, pageSize: number): SensitiveWord[] {
const words = ['违规', '垃圾广告', '诈骗', '恶意攻击', '敏感信息', '暴力', '赌博', '毒品']
const levels: Array<'高' | '中' | '低'> = ['高', '中', '低']
const categories = ['政治', '色情', '广告', '欺诈', '暴力']
const result: SensitiveWord[] = []
const start = (pageNum - 1) * pageSize
for (let i = 0; i < pageSize; i++) {
const idx = start + i
if (idx >= 63) break
result.push({
id: `word_${idx}`,
word: `${words[idx % words.length]}_${idx + 1}`,
level: levels[idx % levels.length],
category: categories[idx % categories.length],
enabled: idx % 3 !== 0,
updatedAt: new Date(Date.now() - idx * 86400000).toISOString(),
})
}
return result
}
function openDialog(row?: SensitiveWord) { function openDialog(row?: SensitiveWord) {
isEdit.value = !!row isEdit.value = !!row
dialogTitle.value = row ? '编辑敏感词' : '新增敏感词' dialogTitle.value = row ? '编辑敏感词' : '新增敏感词'
@ -220,13 +211,11 @@ async function submitForm() {
if (!valid) return if (!valid) return
submitLoading.value = true submitLoading.value = true
try { try {
// TODO: if (isEdit.value && form.id) {
// if (isEdit.value && form.id) { await opsApi.updateSensitiveWord(form.id, form as SensitiveWord)
// await opsApi.updateSensitiveWord(form.id, form as SensitiveWord) } else {
// } else { await opsApi.createSensitiveWord(form as SensitiveWord)
// await opsApi.createSensitiveWord(form as SensitiveWord) }
// }
await new Promise(r => setTimeout(r, 400))
ElMessage.success(isEdit.value ? '编辑成功' : '新增成功') ElMessage.success(isEdit.value ? '编辑成功' : '新增成功')
dialogVisible.value = false dialogVisible.value = false
loadWords() loadWords()
@ -239,8 +228,7 @@ async function submitForm() {
async function toggleWord(row: SensitiveWord) { async function toggleWord(row: SensitiveWord) {
try { try {
// TODO: await opsApi.toggleSensitiveWord(row.id, row.enabled)
// await opsApi.toggleSensitiveWord(row.id, row.enabled)
ElMessage.success('状态已更新') ElMessage.success('状态已更新')
} catch { } catch {
row.enabled = !row.enabled row.enabled = !row.enabled
@ -251,8 +239,7 @@ async function toggleWord(row: SensitiveWord) {
async function deleteWord(row: SensitiveWord) { async function deleteWord(row: SensitiveWord) {
try { try {
await ElMessageBox.confirm(`确定删除敏感词「${row.word}」吗?`, '提示', { type: 'warning' }) await ElMessageBox.confirm(`确定删除敏感词「${row.word}」吗?`, '提示', { type: 'warning' })
// TODO: await opsApi.deleteSensitiveWord(row.id)
// await opsApi.deleteSensitiveWord(row.id)
ElMessage.success('删除成功') ElMessage.success('删除成功')
loadWords() loadWords()
} catch { } catch {
@ -265,6 +252,7 @@ function fmt(value: string) {
} }
onMounted(() => { onMounted(() => {
loadRules()
loadWords() loadWords()
}) })
</script> </script>

查看文件

@ -58,13 +58,13 @@ let trendChart: echarts.ECharts | null = null
let pieChart: echarts.ECharts | null = null let pieChart: echarts.ECharts | null = null
let barChart: echarts.ECharts | null = null let barChart: echarts.ECharts | null = null
function initTrendChart() { function initTrendChart(dailyTrend: Array<{ date: string; count: number }>) {
if (!trendChartRef.value) return if (!trendChartRef.value) return
const dates = Array.from({ length: 7 }, (_, i) => { const dates = dailyTrend.map(d => {
const d = new Date() const parts = d.date.split('-')
d.setDate(d.getDate() - (6 - i)) return `${parseInt(parts[1])}/${parseInt(parts[2])}`
return `${d.getMonth() + 1}/${d.getDate()}`
}) })
const values = dailyTrend.map(d => d.count)
trendChart = echarts.init(trendChartRef.value) trendChart = echarts.init(trendChartRef.value)
trendChart.setOption({ trendChart.setOption({
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
@ -75,7 +75,7 @@ function initTrendChart() {
name: '新增注册', name: '新增注册',
type: 'line', type: 'line',
smooth: true, smooth: true,
data: [12, 19, 8, 24, 32, 18, 27], data: values,
areaStyle: { areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(64,158,255,0.3)' }, { offset: 0, color: 'rgba(64,158,255,0.3)' },
@ -87,8 +87,12 @@ function initTrendChart() {
}) })
} }
function initPieChart() { function initPieChart(serviceDistribution: Record<string, number>) {
if (!pieChartRef.value) return if (!pieChartRef.value) return
const data = Object.entries(serviceDistribution).map(([name, value]) => ({ name, value }))
if (data.length === 0) {
data.push({ name: '暂无数据', value: 0 })
}
pieChart = echarts.init(pieChartRef.value) pieChart = echarts.init(pieChartRef.value)
pieChart.setOption({ pieChart.setOption({
tooltip: { trigger: 'item' }, tooltip: { trigger: 'item' },
@ -100,13 +104,7 @@ function initPieChart() {
itemStyle: { borderRadius: 8, borderColor: '#fff', borderWidth: 2 }, itemStyle: { borderRadius: 8, borderColor: '#fff', borderWidth: 2 },
label: { show: false }, label: { show: false },
emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } }, emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } },
data: [ data,
{ value: 1048, name: 'IM' },
{ value: 735, name: 'Push' },
{ value: 580, name: 'Update' },
{ value: 484, name: 'Webhook' },
{ value: 300, name: '灰度发布' },
],
}], }],
}) })
} }
@ -126,11 +124,10 @@ function initBarChart() {
xAxis: { type: 'category', data: dates }, xAxis: { type: 'category', data: dates },
yAxis: { type: 'value' }, yAxis: { type: 'value' },
series: [ series: [
{ name: '发送消息', type: 'bar', data: [120, 132, 101, 134, 90, 230, 210], itemStyle: { color: '#409eff' } }, { name: '发送消息', type: 'bar', data: [0, 0, 0, 0, 0, 0, 0], itemStyle: { color: '#409eff' } },
{ name: '接收消息', type: 'bar', data: [220, 182, 191, 234, 290, 330, 310], itemStyle: { color: '#67c23a' } }, { name: '接收消息', type: 'bar', data: [0, 0, 0, 0, 0, 0, 0], itemStyle: { color: '#67c23a' } },
], ],
}) })
// TODO:
} }
function handleResize() { function handleResize() {
@ -147,10 +144,13 @@ onMounted(async () => {
stats.value[1].value = d.todayNew ?? 0 stats.value[1].value = d.todayNew ?? 0
stats.value[2].value = d.activeApps ?? 0 stats.value[2].value = d.activeApps ?? 0
stats.value[3].value = d.onlineUsers ?? 0 stats.value[3].value = d.onlineUsers ?? 0
} catch {} initTrendChart(d.dailyTrend ?? [])
initPieChart(d.serviceDistribution ?? {})
} catch {
initTrendChart([])
initPieChart({})
}
initTrendChart()
initPieChart()
initBarChart() initBarChart()
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
}) })

查看文件

@ -165,6 +165,8 @@ export interface WebhookConfig {
secret?: string | null secret?: string | null
enabled: boolean enabled: boolean
createdAt: number createdAt: number
consecutiveFailures?: number
lastFailureAt?: number | null
} }
export interface WebhookConfigForm { export interface WebhookConfigForm {
@ -173,6 +175,32 @@ export interface WebhookConfigForm {
enabled?: boolean enabled?: boolean
} }
export interface WebhookDelivery {
id: string
appId: string
callbackId: string
callbackEvent: string
url: string
httpStatus: number
responseBody?: string | null
errorMessage?: string | null
attempt: number
success: boolean
createdAt: number
}
export interface WebhookAlert {
id: string
appId: string
webhookId: string
webhookUrl: string
alertType: string
description?: string | null
acknowledged: boolean
createdAt: number
acknowledgedAt?: number | null
}
export interface GroupJoinRequest { export interface GroupJoinRequest {
id: string id: string
appId: string appId: string
@ -517,4 +545,82 @@ export const imAdminApi = {
}, },
) )
}, },
transferGroupOwner(appId: string, groupId: string, newOwnerId: string) {
return imClient.post<{ data: ImGroup }>(
`/api/im/admin/groups/${encodeURIComponent(groupId)}/owner`,
{ newOwnerId },
{ params: { appId } },
)
},
updateGroupAttributes(appId: string, groupId: string, attributes: Record<string, unknown>) {
return imClient.put<{ data: ImGroup }>(
`/api/im/admin/groups/${encodeURIComponent(groupId)}/attributes`,
{ attributes },
{ params: { appId } },
)
},
removeGroupAttributes(appId: string, groupId: string, keys: string[]) {
return imClient.post<{ data: ImGroup }>(
`/api/im/admin/groups/${encodeURIComponent(groupId)}/attributes/delete`,
{ keys },
{ params: { appId } },
)
},
getGroupReadReceipts(appId: string, groupId: string, messageIds?: string[]) {
return imClient.post<{ data: Array<{ messageId: string; readCount: number; readUserIds: string[] }> }>(
`/api/im/admin/groups/${encodeURIComponent(groupId)}/read-receipts`,
{ messageIds },
{ params: { appId } },
)
},
queryUserState(appId: string, userIds: string[]) {
return imClient.get<{ data: Record<string, { online: boolean; lastSeenAt: number }> }>(
'/api/im/admin/users/state',
{ params: { userIds: userIds.join(',') } },
)
},
listWebhookDeliveries(
appId: string,
params: { callbackEvent?: string; success?: boolean; page?: number; size?: number } = {},
) {
return imClient.get<{ data: PagedResult<WebhookDelivery> }>('/api/im/admin/webhook-deliveries', {
params: { appId, ...params },
})
},
listWebhookAlerts(
appId: string,
params: { acknowledged?: boolean; page?: number; size?: number } = {},
) {
return imClient.get<{ data: PagedResult<WebhookAlert> }>('/api/im/admin/webhook-alerts', {
params: { appId, ...params },
})
},
acknowledgeWebhookAlert(appId: string, alertId: string) {
return imClient.post<{ data: WebhookAlert }>(
`/api/im/admin/webhook-alerts/${encodeURIComponent(alertId)}/acknowledge`,
null,
{ params: { appId } },
)
},
getWebhookHealth(appId: string, webhookId: string) {
return imClient.get<{
data: {
webhookId: string
url: string
enabled: boolean
consecutiveFailures: number
lastFailureAt: number | null
unacknowledgedAlerts: number
}
}>(`/api/im/admin/webhooks/${encodeURIComponent(webhookId)}/health`, { params: { appId } })
},
} }

查看文件

@ -61,6 +61,14 @@ const router = createRouter({
path: 'apps/:appId/im-webhooks', path: 'apps/:appId/im-webhooks',
component: () => import('@/views/im/ImWebhookView.vue'), component: () => import('@/views/im/ImWebhookView.vue'),
}, },
{
path: 'apps/:appId/im-webhooks/:webhookId/deliveries',
component: () => import('@/views/im/WebhookDeliveryLogView.vue'),
},
{
path: 'apps/:appId/im-webhook-alerts',
component: () => import('@/views/im/WebhookAlertView.vue'),
},
{ {
path: 'apps/:appId/im', path: 'apps/:appId/im',
component: () => import('@/views/im/ImManagementView.vue'), component: () => import('@/views/im/ImManagementView.vue'),

查看文件

@ -44,6 +44,15 @@
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="在线" width="120">
<template #default="{ row }">
<el-tag v-if="userOnlineState[row.userId]?.online" type="success" size="small">在线</el-tag>
<el-tag v-else-if="userOnlineState[row.userId]?.lastSeenAt" type="info" size="small">
{{ formatLastSeen(userOnlineState[row.userId].lastSeenAt) }}
</el-tag>
<el-tag v-else type="info" size="small">离线</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="注册时间" width="180"> <el-table-column prop="createdAt" label="注册时间" width="180">
<template #default="{ row }"> <template #default="{ row }">
{{ formatTime(row.createdAt) }} {{ formatTime(row.createdAt) }}
@ -470,6 +479,11 @@
<el-descriptions-item label="公告">{{ managedGroup.announcement || '-' }}</el-descriptions-item> <el-descriptions-item label="公告">{{ managedGroup.announcement || '-' }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
<div v-if="managedGroup" class="toolbar" style="margin-bottom: 12px">
<el-button type="primary" size="small" @click="openGroupAttributesDialog">扩展属性</el-button>
<el-button type="info" size="small" @click="openReadReceiptsDialog">回执查询</el-button>
</div>
<el-tabs v-model="groupManageTab"> <el-tabs v-model="groupManageTab">
<el-tab-pane label="成员" name="members"> <el-tab-pane label="成员" name="members">
<div class="toolbar toolbar-space-between"> <div class="toolbar toolbar-space-between">
@ -519,8 +533,17 @@
<el-table-column prop="createdAt" label="加入时间" width="180"> <el-table-column prop="createdAt" label="加入时间" width="180">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template> <template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="260" fixed="right"> <el-table-column label="操作" width="320" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button
v-if="row.userId !== managedGroup?.creatorId"
link
type="success"
size="small"
@click="transferGroupOwner(row)"
>
转让群主
</el-button>
<el-button <el-button
link link
type="primary" type="primary"
@ -606,6 +629,34 @@
</el-tabs> </el-tabs>
</el-dialog> </el-dialog>
<el-dialog v-model="showGroupAttributesDialog" title="群扩展属性" width="560px">
<el-form label-width="100px">
<el-form-item label="属性 JSON">
<el-input v-model="groupAttributesJson" type="textarea" :rows="6" placeholder='{"key": "value"}' />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showGroupAttributesDialog = false">取消</el-button>
<el-button type="primary" @click="submitGroupAttributes">保存</el-button>
</template>
</el-dialog>
<el-dialog v-model="showReadReceiptsDialog" title="群消息已读回执" width="640px">
<div class="toolbar">
<el-button @click="loadGroupReadReceipts" :loading="loadingReadReceipts">刷新</el-button>
</div>
<el-table :data="groupReadReceipts" v-loading="loadingReadReceipts" border stripe>
<el-table-column prop="messageId" label="消息ID" min-width="200" />
<el-table-column prop="readCount" label="已读人数" width="100" />
<el-table-column prop="readUserIds" label="已读用户" min-width="240">
<template #default="{ row }">
<span v-if="row.readUserIds && row.readUserIds.length">{{ row.readUserIds.join(', ') }}</span>
<span v-else class="el-text el-text--info"></span>
</template>
</el-table-column>
</el-table>
</el-dialog>
<el-dialog <el-dialog
v-model="showWebhookDialog" v-model="showWebhookDialog"
:title="editingWebhookId ? '编辑回调配置' : '新增回调配置'" :title="editingWebhookId ? '编辑回调配置' : '新增回调配置'"
@ -737,6 +788,7 @@ const loadingUsers = ref(false)
const userPage = ref(0) const userPage = ref(0)
const userPageSize = 20 const userPageSize = 20
const userTotal = ref(0) const userTotal = ref(0)
const userOnlineState = ref<Record<string, { online: boolean; lastSeenAt: number }>>({})
const groups = ref<ImGroup[]>([]) const groups = ref<ImGroup[]>([])
const loadingGroups = ref(false) const loadingGroups = ref(false)
@ -841,6 +893,13 @@ const editGroupForm = ref({
announcement: '', announcement: '',
}) })
const showGroupAttributesDialog = ref(false)
const groupAttributesJson = ref('{}')
const showReadReceiptsDialog = ref(false)
const groupReadReceipts = ref<Array<{ messageId: string; readCount: number; readUserIds: string[] }>>([])
const loadingReadReceipts = ref(false)
const memberSearchKeyword = ref('') const memberSearchKeyword = ref('')
const memberSearchResults = ref<ImUser[]>([]) const memberSearchResults = ref<ImUser[]>([])
const searchingMembers = ref(false) const searchingMembers = ref(false)
@ -1093,6 +1152,15 @@ async function loadUsers() {
const res = await imAdminApi.listUsers(appKey.value, userPage.value, userPageSize) const res = await imAdminApi.listUsers(appKey.value, userPage.value, userPageSize)
users.value = res.data.data.content users.value = res.data.data.content
userTotal.value = res.data.data.totalElements userTotal.value = res.data.data.totalElements
if (users.value.length > 0) {
const ids = users.value.map(u => u.userId)
try {
const stateRes = await imAdminApi.queryUserState(appKey.value, ids)
userOnlineState.value = stateRes.data.data
} catch {
// ignore state query error
}
}
} catch { } catch {
ElMessage.error('加载用户失败') ElMessage.error('加载用户失败')
} finally { } finally {
@ -1100,6 +1168,18 @@ async function loadUsers() {
} }
} }
function formatLastSeen(ts: number): string {
if (!ts) return '未知'
const diff = Date.now() - ts
const minutes = Math.floor(diff / 60000)
if (minutes < 1) return '刚刚'
if (minutes < 60) return `${minutes}分钟前`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours}小时前`
const days = Math.floor(hours / 24)
return `${days}天前`
}
async function loadGroups() { async function loadGroups() {
loadingGroups.value = true loadingGroups.value = true
try { try {
@ -1297,6 +1377,59 @@ async function removeManagedGroupMember(user: ImUser) {
await loadGroups() await loadGroups()
} }
async function transferGroupOwner(user: ImUser) {
const group = managedGroup.value
if (!group) return
await ElMessageBox.confirm(`确认将群主转让给 ${user.userId}`, '转让群主', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消',
})
await imAdminApi.transferGroupOwner(appKey.value, group.id, user.userId)
ElMessage.success('群主已转让')
await loadManagedGroupMembers()
await loadGroups()
}
function openGroupAttributesDialog() {
const group = managedGroup.value
if (!group) return
groupAttributesJson.value = '{}'
showGroupAttributesDialog.value = true
}
async function submitGroupAttributes() {
const group = managedGroup.value
if (!group) return
try {
const attributes = JSON.parse(groupAttributesJson.value)
await imAdminApi.updateGroupAttributes(appKey.value, group.id, attributes)
ElMessage.success('扩展属性已保存')
showGroupAttributesDialog.value = false
} catch {
ElMessage.error('JSON 格式错误')
}
}
function openReadReceiptsDialog() {
showReadReceiptsDialog.value = true
loadGroupReadReceipts()
}
async function loadGroupReadReceipts() {
const group = managedGroup.value
if (!group) return
loadingReadReceipts.value = true
try {
const res = await imAdminApi.getGroupReadReceipts(appKey.value, group.id)
groupReadReceipts.value = res.data.data ?? []
} catch {
groupReadReceipts.value = []
} finally {
loadingReadReceipts.value = false
}
}
async function toggleManagedGroupRole(user: ImUser) { async function toggleManagedGroupRole(user: ImUser) {
const group = managedGroup.value const group = managedGroup.value
if (!group) return if (!group) return

查看文件

@ -40,8 +40,15 @@
</el-table> </el-table>
</el-card> </el-card>
<el-card> <el-card style="margin-bottom:16px">
<template #header>回调地址</template> <template #header>
<div class="toolbar toolbar-space-between">
<span>回调地址</span>
<el-button link type="primary" @click="$router.push({ path: `/apps/${appId}/im-webhook-alerts` })">
查看告警
</el-button>
</div>
</template>
<div class="toolbar toolbar-space-between"> <div class="toolbar toolbar-space-between">
<el-button type="primary" @click="openCreateWebhookDialog">新增回调</el-button> <el-button type="primary" @click="openCreateWebhookDialog">新增回调</el-button>
<el-button @click="loadWebhooks" :loading="loadingWebhooks">刷新</el-button> <el-button @click="loadWebhooks" :loading="loadingWebhooks">刷新</el-button>
@ -54,19 +61,32 @@
{{ row.secret ? '******' : '-' }} {{ row.secret ? '******' : '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="enabled" label="启用" width="100"> <el-table-column prop="enabled" label="启用" width="90">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="row.enabled ? 'success' : 'info'" size="small"> <el-tag :type="row.enabled ? 'success' : 'info'" size="small">
{{ row.enabled ? '启用' : '停用' }} {{ row.enabled ? '启用' : '停用' }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="180"> <el-table-column label="健康" width="120">
<template #default="{ row }">
<el-tag
v-if="row.consecutiveFailures && row.consecutiveFailures > 0"
:type="row.consecutiveFailures >= 10 ? 'danger' : 'warning'"
size="small"
>
失败 {{ row.consecutiveFailures }}
</el-tag>
<el-tag v-else type="success" size="small">正常</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="170">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template> <template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="180" fixed="right"> <el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" size="small" @click="openEditWebhookDialog(row)">编辑</el-button> <el-button link type="primary" size="small" @click="openEditWebhookDialog(row)">编辑</el-button>
<el-button link type="info" size="small" @click="$router.push({ path: `/apps/${appId}/im-webhooks/${row.id}/deliveries` })">日志</el-button>
<el-button link type="danger" size="small" @click="deleteWebhook(row)">删除</el-button> <el-button link type="danger" size="small" @click="deleteWebhook(row)">删除</el-button>
</template> </template>
</el-table-column> </el-table-column>

查看文件

@ -0,0 +1,133 @@
<template>
<div v-if="app">
<el-page-header @back="$router.back()" content="Webhook 告警" style="margin-bottom:24px" />
<el-card style="margin-bottom:16px">
<el-descriptions :column="2" border>
<el-descriptions-item label="应用名称">{{ app.name }}</el-descriptions-item>
<el-descriptions-item label="未确认告警">
<el-tag v-if="unacknowledgedCount > 0" type="danger">{{ unacknowledgedCount }}</el-tag>
<span v-else>0</span>
</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card>
<template #header>告警列表</template>
<div class="toolbar toolbar-space-between" style="margin-bottom:16px">
<div style="display:flex;gap:12px;align-items:center">
<el-select v-model="filterAcknowledged" placeholder="确认状态" clearable style="width:140px">
<el-option label="未确认" :value="false" />
<el-option label="已确认" :value="true" />
</el-select>
<el-button @click="loadAlerts" :loading="loading">查询</el-button>
</div>
</div>
<el-table :data="alerts" v-loading="loading" border stripe>
<el-table-column prop="webhookUrl" label="回调地址" min-width="260" show-overflow-tooltip />
<el-table-column prop="alertType" label="类型" width="120">
<template #default="{ row }">
<el-tag v-if="row.alertType === 'AUTO_DISABLED'" type="danger" size="small">自动禁用</el-tag>
<el-tag v-else type="warning" size="small">{{ row.alertType }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="说明" min-width="240" show-overflow-tooltip />
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag :type="row.acknowledged ? 'info' : 'danger'" size="small">
{{ row.acknowledged ? '已确认' : '未确认' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="告警时间" width="170">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button
v-if="!row.acknowledged"
link
type="primary"
size="small"
@click="acknowledge(row)"
>确认</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="page"
v-model:page-size="size"
:total="total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
style="margin-top:16px"
@change="loadAlerts"
/>
</el-card>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { appApi, type App } from '@/api/app'
import { imAdminApi, type WebhookAlert } from '@/api/im'
const route = useRoute()
const app = ref<App | null>(null)
const appId = computed(() => route.params.appId as string)
const alerts = ref<WebhookAlert[]>([])
const loading = ref(false)
const page = ref(1)
const size = ref(20)
const total = ref(0)
const filterAcknowledged = ref<boolean | ''>('')
const unacknowledgedCount = ref(0)
async function loadApp() {
const res = await appApi.get(appId.value)
app.value = res.data.data
}
async function loadAlerts() {
loading.value = true
try {
const params: Record<string, unknown> = { page: page.value - 1, size: size.value }
if (filterAcknowledged.value !== '') params.acknowledged = filterAcknowledged.value
const res = await imAdminApi.listWebhookAlerts(appId.value, params)
alerts.value = res.data.data.content
total.value = res.data.data.totalElements
} finally {
loading.value = false
}
}
async function loadUnacknowledgedCount() {
const res = await imAdminApi.listWebhookAlerts(appId.value, { acknowledged: false, size: 1 })
unacknowledgedCount.value = Number(res.data.data.totalElements)
}
async function acknowledge(row: WebhookAlert) {
await imAdminApi.acknowledgeWebhookAlert(appId.value, row.id)
ElMessage.success('已确认')
await loadAlerts()
await loadUnacknowledgedCount()
}
function formatTime(value: number) {
return new Date(value).toLocaleString()
}
watch([filterAcknowledged], () => {
page.value = 1
loadAlerts()
})
onMounted(async () => {
await Promise.all([loadApp(), loadAlerts(), loadUnacknowledgedCount()])
})
</script>

查看文件

@ -0,0 +1,118 @@
<template>
<div v-if="app">
<el-page-header @back="$router.back()" content="Webhook 投递日志" style="margin-bottom:24px" />
<el-card style="margin-bottom:16px">
<el-descriptions :column="2" border>
<el-descriptions-item label="应用名称">{{ app.name }}</el-descriptions-item>
<el-descriptions-item label="回调地址">{{ webhookUrl }}</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card>
<template #header>投递记录</template>
<div class="toolbar toolbar-space-between" style="margin-bottom:16px">
<div style="display:flex;gap:12px;align-items:center">
<el-select v-model="filterEvent" placeholder="事件类型" clearable style="width:180px">
<el-option label="message.sent" value="message.sent" />
<el-option label="message.revoked" value="message.revoked" />
<el-option label="message.edited" value="message.edited" />
<el-option label="message.read" value="message.read" />
</el-select>
<el-select v-model="filterSuccess" placeholder="状态" clearable style="width:120px">
<el-option label="成功" :value="true" />
<el-option label="失败" :value="false" />
</el-select>
<el-button @click="loadDeliveries" :loading="loading">查询</el-button>
</div>
</div>
<el-table :data="deliveries" v-loading="loading" border stripe>
<el-table-column prop="callbackEvent" label="事件" width="160" />
<el-table-column prop="attempt" label="重试次数" width="90" />
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag :type="row.success ? 'success' : 'danger'" size="small">
{{ row.success ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="httpStatus" label="HTTP" width="90" />
<el-table-column prop="errorMessage" label="错误信息" min-width="200" show-overflow-tooltip />
<el-table-column prop="responseBody" label="响应体" min-width="200" show-overflow-tooltip />
<el-table-column prop="createdAt" label="时间" width="170">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="page"
v-model:page-size="size"
:total="total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
style="margin-top:16px"
@change="loadDeliveries"
/>
</el-card>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { appApi, type App } from '@/api/app'
import { imAdminApi, type WebhookDelivery } from '@/api/im'
const route = useRoute()
const app = ref<App | null>(null)
const appId = computed(() => route.params.appId as string)
const webhookId = computed(() => route.params.webhookId as string)
const webhookUrl = ref('')
const deliveries = ref<WebhookDelivery[]>([])
const loading = ref(false)
const page = ref(1)
const size = ref(20)
const total = ref(0)
const filterEvent = ref('')
const filterSuccess = ref<boolean | ''>('')
async function loadApp() {
const res = await appApi.get(appId.value)
app.value = res.data.data
}
async function loadWebhook() {
const res = await imAdminApi.listWebhooks(appId.value)
const wh = res.data.data.find((w) => w.id === webhookId.value)
if (wh) webhookUrl.value = wh.url
}
async function loadDeliveries() {
loading.value = true
try {
const params: Record<string, unknown> = { page: page.value - 1, size: size.value }
if (filterEvent.value) params.callbackEvent = filterEvent.value
if (filterSuccess.value !== '') params.success = filterSuccess.value
const res = await imAdminApi.listWebhookDeliveries(appId.value, params)
deliveries.value = res.data.data.content
total.value = res.data.data.totalElements
} finally {
loading.value = false
}
}
function formatTime(value: number) {
return new Date(value).toLocaleString()
}
watch([filterEvent, filterSuccess], () => {
page.value = 1
loadDeliveries()
})
onMounted(async () => {
await Promise.all([loadApp(), loadWebhook(), loadDeliveries()])
})
</script>